lJava发射(案例)
l反射含义:
lJAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
lJAVA反射(放射)机制:“程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言”。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。但是JAVA有着一个非常突出的动态相关机制:Reflection,用在Java身上指的是我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。
l【案例1】通过一个对象获得完整的包名和类名
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 |
lpackageReflect;
l/** l* 通过一个对象获得完整的包名和类名 l* */ lclassDemo{ l//other codes... l}
lclasshello{ lpublicstaticvoidmain(String[] args) { lDemo demo=newDemo(); lSystem.out.println(demo.getClass().getName()); l} l} |
l【运行结果】:Reflect.Demo
l添加一句:所有类的对象其实都是Class的实例。
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 |
lpackageReflect; lclassDemo{ l//other codes... l}
lclasshello{ lpublicstaticvoidmain(String[] args) { lClass<?> demo1=null; lClass<?> demo2=null; lClass<?> demo3=null; ltry{ l//一般尽量采用这种形式 ldemo1=Class.forName("Reflect.Demo"); l}catch(Exception e){ le.printStackTrace(); l} ldemo2=newDemo().getClass(); ldemo3=Demo.class;
lSystem.out.println("类名称 "+demo1.getName()); lSystem.out.println("类名称 "+demo2.getName()); lSystem.out.println("类名称 "+demo3.getName());
l} l} |
l【案例2】实例化Class类对象
l【运行结果】:
l类名称 Reflect.Demo
l类名称 Reflect.Demo
l类名称 Reflect.Demo
l【案例3】通过Class实例化其他类的对象
l通过无参构造实例化对象
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39 l40 l41 l42 l43 l44 l45 l46 l47 |
lpackageReflect;
lclassPerson{
lpublicString getName() { lreturnname; l} lpublicvoidsetName(String name) { lthis.name = name; l} lpublicintgetAge() { lreturnage; l} lpublicvoidsetAge(intage) { lthis.age = age; l} l@Override lpublicString toString(){ lreturn"["+this.name+" "+this.age+"]"; l} lprivateString name; lprivateintage; l}
lclasshello{ lpublicstaticvoidmain(String[] args) { lClass<?> demo=null; ltry{ ldemo=Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} lPerson per=null; ltry{ lper=(Person)demo.newInstance(); l}catch(InstantiationException e) { l// TODO Auto-generated catch block le.printStackTrace(); l}catch(IllegalAccessException e) { l// TODO Auto-generated catch block le.printStackTrace(); l} lper.setName("Rollen"); lper.setAge(20); lSystem.out.println(per); l} l} |
l【运行结果】:
l[Rollen 20]
l但是注意一下,当我们把Person中的默认的无参构造函数取消的时候,比如自己定义只定义一个有参数的构造函数之后,会出现错误:
l比如我定义了一个构造函数:
l1 l2 l3 l4 |
lpublicPerson(String name,intage) { lthis.age=age; lthis.name=name; l} |
l然后继续运行上面的程序,会出现:
ljava.lang.InstantiationException: Reflect.Person
lat java.lang.Class.newInstance0(Class.java:340)
lat java.lang.Class.newInstance(Class.java:308)
lat Reflect.hello.main(hello.java:39)
lException in thread "main"java.lang.NullPointerException
lat Reflect.hello.main(hello.java:47)
l所以大家以后再编写使用Class实例化其他类的对象的时候,一定要自己定义无参的构造函数
l【案例3】通过Class调用其他类中的构造函数(也可以通过这种方式通过Class创建其他类的对象)
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39 l40 l41 l42 l43 l44 l45 l46 l47 l48 l49 l50 l51 l52 l53 l54 l55 l56 l57 l58 l59 l60 l61 |
lpackageReflect;
limportjava.lang.reflect.Constructor;
lclassPerson{
lpublicPerson() {
l} lpublicPerson(String name){ lthis.name=name; l} lpublicPerson(intage){ lthis.age=age; l} lpublicPerson(String name,intage) { lthis.age=age; lthis.name=name; l} lpublicString getName() { lreturnname; l} lpublicintgetAge() { lreturnage; l} l@Override lpublicString toString(){ lreturn"["+this.name+" "+this.age+"]"; l} lprivateString name; lprivateintage; l}
lclasshello{ lpublicstaticvoidmain(String[] args) { lClass<?> demo=null; ltry{ ldemo=Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} lPerson per1=null; lPerson per2=null; lPerson per3=null; lPerson per4=null; l//取得全部的构造函数 lConstructor<?> cons[]=demo.getConstructors(); ltry{ lper1=(Person)cons[0].newInstance(); lper2=(Person)cons[1].newInstance("Rollen"); lper3=(Person)cons[2].newInstance(20); lper4=(Person)cons[3].newInstance("Rollen",20); l}catch(Exception e){ le.printStackTrace(); l} lSystem.out.println(per1); lSystem.out.println(per2); lSystem.out.println(per3); lSystem.out.println(per4); l} l} |
l【运行结果】:
l[null 0]
l[Rollen 0]
l[null 20]
l[Rollen 20]
l【案例】
l返回一个类实现的接口:
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39 l40 l41 l42 l43 l44 l45 l46 l47 l48 |
lpackageReflect;
linterfaceChina{ lpublicstaticfinalString name="Rollen"; lpublicstaticintage=20; lpublicvoidsayChina(); lpublicvoidsayHello(String name,intage); l}
lclassPersonimplementsChina{ lpublicPerson() {
l} lpublicPerson(String sex){ lthis.sex=sex; l} lpublicString getSex() { lreturnsex; l} lpublicvoidsetSex(String sex) { lthis.sex = sex; l} l@Override lpublicvoidsayChina(){ lSystem.out.println("hello ,china"); l} l@Override lpublicvoidsayHello(String name,intage){ lSystem.out.println(name+" "+age); l} lprivateString sex; l}
lclasshello{ lpublicstaticvoidmain(String[] args) { lClass<?> demo=null; ltry{ ldemo=Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} l//保存所有的接口 lClass<?> intes[]=demo.getInterfaces(); lfor(inti =0; i < intes.length; i++) { lSystem.out.println("实现的接口 "+intes[i].getName()); l} l} l} |
l【运行结果】:
l实现的接口 Reflect.China
l(注意,以下几个例子,都会用到这个例子的Person类,所以为节省篇幅,此处不再粘贴Person的代码部分,只粘贴主类hello的代码)
l【案例4】:取得其他类中的父类
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 |
lclasshello{ lpublicstaticvoidmain(String[] args) { lClass<?> demo=null; ltry{ ldemo=Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} l//取得父类 lClass<?> temp=demo.getSuperclass(); lSystem.out.println("继承的父类为: "+temp.getName()); l} l} |
l【运行结果】
l继承的父类为: java.lang.Object
l【案例】:获得其他类中的全部构造函数
l这个例子需要在程序开头添加importjava.lang.reflect.*;
l然后将主类编写为:
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 |
lclasshello{ lpublicstaticvoidmain(String[] args) { lClass<?> demo=null; ltry{ ldemo=Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} lConstructor<?>cons[]=demo.getConstructors(); lfor(inti =0; i < cons.length; i++) { lSystem.out.println("构造方法: "+cons[i]); l} l} l} |
l【运行结果】:
l构造方法: public Reflect.Person()
l构造方法: public Reflect.Person(java.lang.String)
l但是细心的读者会发现,上面的构造函数没有public或者private这一类的修饰符
l下面这个例子我们就来获取修饰符
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 |
lclasshello{ lpublicstaticvoidmain(String[] args) { lClass<?> demo=null; ltry{ ldemo=Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} lConstructor<?>cons[]=demo.getConstructors(); lfor(inti =0; i < cons.length; i++) { lClass<?> p[]=cons[i].getParameterTypes(); lSystem.out.print("构造方法: "); lintmo=cons[i].getModifiers(); lSystem.out.print(Modifier.toString(mo)+" "); lSystem.out.print(cons[i].getName()); lSystem.out.print("("); lfor(intj=0;j<p.length;++j){ lSystem.out.print(p[j].getName()+" arg"+i); lif(j<p.length-1){ lSystem.out.print(","); l} l} lSystem.out.println("){}"); l} l} l} |
l【运行结果】:
l构造方法: public Reflect.Person(){}
l构造方法: public Reflect.Person(java.lang.String arg1){}
l有时候一个方法可能还有异常,呵呵。下面看看:
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39 |
lclasshello{ lpublicstaticvoidmain(String[] args) { lClass<?> demo=null; ltry{ ldemo=Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} lMethod method[]=demo.getMethods(); lfor(inti=0;i<method.length;++i){ lClass<?> returnType=method[i].getReturnType(); lClass<?> para[]=method[i].getParameterTypes(); linttemp=method[i].getModifiers(); lSystem.out.print(Modifier.toString(temp)+" "); lSystem.out.print(returnType.getName()+" "); lSystem.out.print(method[i].getName()+" "); lSystem.out.print("("); lfor(intj=0;j<para.length;++j){ lSystem.out.print(para[j].getName()+" "+"arg"+j); lif(j<para.length-1){ lSystem.out.print(","); l} l} lClass<?> exce[]=method[i].getExceptionTypes(); lif(exce.length>0){ lSystem.out.print(") throws "); lfor(intk=0;k<exce.length;++k){ lSystem.out.print(exce[k].getName()+" "); lif(k<exce.length-1){ lSystem.out.print(","); l} l} l}else{ lSystem.out.print(")"); l} lSystem.out.println(); l} l} l} |
l【运行结果】:
lpublic java.lang.String getSex ()
lpublic void setSex (java.lang.String arg0)
lpublic void sayChina ()
lpublic void sayHello (java.lang.String arg0,int arg1)
lpublic final native void wait (long arg0) throwsjava.lang.InterruptedException
lpublic final void wait () throwsjava.lang.InterruptedException
lpublic final void wait (long arg0,int arg1) throwsjava.lang.InterruptedException
lpublic boolean equals (java.lang.Object arg0)
lpublic java.lang.String toString ()
lpublic native int hashCode ()
lpublic final native java.lang.Class getClass ()
lpublic final native void notify ()
lpublic final native void notifyAll ()
l【案例6】接下来让我们取得其他类的全部属性吧,最后我讲这些整理在一起,也就是通过class取得一个类的全部框架
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 |
lclasshello { lpublicstaticvoidmain(String[] args) { lClass<?> demo =null; ltry{ ldemo = Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} lSystem.out.println("===============本类属性========================"); l// 取得本类的全部属性 lField[] field = demo.getDeclaredFields(); lfor(inti =0; i < field.length; i++) { l// 权限修饰符 lintmo = field[i].getModifiers(); lString priv = Modifier.toString(mo); l// 属性类型 lClass<?> type = field[i].getType(); lSystem.out.println(priv +" "+ type.getName() +" " l+ field[i].getName() +";"); l} lSystem.out.println("===============实现的接口或者父类的属性========================"); l// 取得实现的接口或者父类的属性 lField[] filed1 = demo.getFields(); lfor(intj =0; j < filed1.length; j++) { l// 权限修饰符 lintmo = filed1[j].getModifiers(); lString priv = Modifier.toString(mo); l// 属性类型 lClass<?> type = filed1[j].getType(); lSystem.out.println(priv +" "+ type.getName() +" " l+ filed1[j].getName() +";"); l} l} l} |
l【运行结果】:
l===============本类属性========================
lprivate java.lang.String sex;
l===============实现的接口或者父类的属性========================
lpublic static final java.lang.String name;
lpublic static final int age;
l【案例】其实还可以通过反射调用其他类中的方法:
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 |
lclasshello { lpublicstaticvoidmain(String[] args) { lClass<?> demo =null; ltry{ ldemo = Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} ltry{ l//调用Person类中的sayChina方法 lMethod method=demo.getMethod("sayChina"); lmethod.invoke(demo.newInstance()); l//调用Person的sayHello方法 lmethod=demo.getMethod("sayHello", String.class,int.class); lmethod.invoke(demo.newInstance(),"Rollen",20);
l}catch(Exception e) { le.printStackTrace(); l} l} l} |
l【运行结果】:
lhello ,china
lRollen 20
l【案例】调用其他类的set和get方法
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39 l40 l41 l42 l43 l44 l45 l46 l47 l48 l49 l50 l51 l52 l53 |
lclasshello { lpublicstaticvoidmain(String[] args) { lClass<?> demo =null; lObject obj=null; ltry{ ldemo = Class.forName("Reflect.Person"); l}catch(Exception e) { le.printStackTrace(); l} ltry{ lobj=demo.newInstance(); l}catch(Exception e) { le.printStackTrace(); l} lsetter(obj,"Sex","男",String.class); lgetter(obj,"Sex"); l}
l/** l* @param obj l* 操作的对象 l* @param att l* 操作的属性 l* */ lpublicstaticvoidgetter(Object obj, String att) { ltry{ lMethod method = obj.getClass().getMethod("get"+ att); lSystem.out.println(method.invoke(obj)); l}catch(Exception e) { le.printStackTrace(); l} l}
l/** l* @param obj l* 操作的对象 l* @param att l* 操作的属性 l* @param value l* 设置的值 l* @param type l* 参数的属性 l* */ lpublicstaticvoidsetter(Object obj, String att, Object value, lClass<?> type) { ltry{ lMethod method = obj.getClass().getMethod("set"+ att, type); lmethod.invoke(obj, value); l}catch(Exception e) { le.printStackTrace(); l} l} l}// end class |
l【运行结果】:
l男
l【案例】通过反射操作属性
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 |
lclasshello { lpublicstaticvoidmain(String[] args)throwsException { lClass<?> demo =null; lObject obj =null;
ldemo = Class.forName("Reflect.Person"); lobj = demo.newInstance();
lField field = demo.getDeclaredField("sex"); lfield.setAccessible(true); lfield.set(obj,"男"); lSystem.out.println(field.get(obj)); l} l}// end class |
l【案例】通过反射取得并修改数组的信息:
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 |
limportjava.lang.reflect.*; lclasshello{ lpublicstaticvoidmain(String[] args) { lint[] temp={1,2,3,4,5}; lClass<?>demo=temp.getClass().getComponentType(); lSystem.out.println("数组类型: "+demo.getName()); lSystem.out.println("数组长度 "+Array.getLength(temp)); lSystem.out.println("数组的第一个元素: "+Array.get(temp,0)); lArray.set(temp,0,100); lSystem.out.println("修改之后数组第一个元素为: "+Array.get(temp,0)); l} l} |
l【运行结果】:
l数组类型:int
l数组长度 5
l数组的第一个元素: 1
l修改之后数组第一个元素为:100
l【案例】通过反射修改数组大小
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 |
lclasshello{ lpublicstaticvoidmain(String[] args) { lint[] temp={1,2,3,4,5,6,7,8,9}; lint[] newTemp=(int[])arrayInc(temp,15); lprint(newTemp); lSystem.out.println("====================="); lString[] atr={"a","b","c"}; lString[] str1=(String[])arrayInc(atr,8); lprint(str1); l}
l/** l* 修改数组大小 l* */ lpublicstaticObject arrayInc(Object obj,intlen){ lClass<?>arr=obj.getClass().getComponentType(); lObject newArr=Array.newInstance(arr, len); lintco=Array.getLength(obj); lSystem.arraycopy(obj,0, newArr,0, co); lreturnnewArr; l} l/** l* 打印 l* */ lpublicstaticvoidprint(Object obj){ lClass<?>c=obj.getClass(); lif(!c.isArray()){ lreturn; l} lSystem.out.println("数组长度为: "+Array.getLength(obj)); lfor(inti =0; i < Array.getLength(obj); i++) { lSystem.out.print(Array.get(obj, i)+" "); l} l} l} |
l【运行结果】:
l数组长度为:15
l1 2 3 4 5 6 7 8 9 0 0 0 0 0 0 =====================
l数组长度为:8
la b c null null null null null
l动态代理
l【案例】首先来看看如何获得类加载器:
l1 l2 l3 l4 l5 l6 l7 l8 l9 |
lclasstest{
l} lclasshello{ lpublicstaticvoidmain(String[] args) { ltest t=newtest(); lSystem.out.println("类加载器 "+t.getClass().getClassLoader().getClass().getName()); l} l} |
l【程序输出】:
l类加载器 sun.misc.Launcher$AppClassLoader
l其实在java中有三种类类加载器。
l1)Bootstrap ClassLoader此加载器采用c++编写,一般开发中很少见。
l2)Extension ClassLoader用来进行扩展类的加载,一般对应的是jre\lib\ext目录中的类
l3)AppClassLoader加载classpath指定的类,是最常用的加载器。同时也是java中默认的加载器。
l如果想要完成动态代理,首先需要定义一个InvocationHandler接口的子类,已完成代理的具体操作。
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39 l40 l41 |
lpackageReflect; limportjava.lang.reflect.*;
l//定义项目接口 linterfaceSubject { lpublicString say(String name,intage); l}
l// 定义真实项目 lclassRealSubjectimplementsSubject { l@Override lpublicString say(String name,intage) { lreturnname +" "+ age; l} l}
lclassMyInvocationHandlerimplementsInvocationHandler { lprivateObject obj =null;
lpublicObject bind(Object obj) { lthis.obj = obj; lreturnProxy.newProxyInstance(obj.getClass().getClassLoader(), obj l.getClass().getInterfaces(),this); l}
l@Override lpublicObject invoke(Object proxy, Method method, Object[] args) lthrowsThrowable { lObject temp = method.invoke(this.obj, args); lreturntemp; l} l}
lclasshello { lpublicstaticvoidmain(String[] args) { lMyInvocationHandler demo =newMyInvocationHandler(); lSubject sub = (Subject) demo.bind(newRealSubject()); lString info = sub.say("Rollen",20); lSystem.out.println(info); l} l} |
l【运行结果】:
lRollen 20
l类的生命周期
l在一个类编译完成之后,下一步就需要开始使用类,如果要使用一个类,肯定离不开JVM。在程序执行中JVM通过装载,链接,初始化这3个步骤完成。
l类的装载是通过类加载器完成的,加载器将.class文件的二进制文件装入JVM的方法区,并且在堆区创建描述这个类的java.lang.Class对象。用来封装数据。但是同一个类只会被类装载器装载以前
l链接就是把二进制数据组装为可以运行的状态。
l链接分为校验,准备,解析这3个阶段
l校验一般用来确认此二进制文件是否适合当前的JVM(版本),
l准备就是为静态成员分配内存空间,。并设置默认值
l解析指的是转换常量池中的代码作为直接引用的过程,直到所有的符号引用都可以被运行程序使用(建立完整的对应关系)
l完成之后,类型也就完成了初始化,初始化之后类的对象就可以正常使用了,直到一个对象不再使用之后,将被垃圾回收。释放空间。
l当没有任何引用指向Class对象时就会被卸载,结束类的生命周期
l将反射用于工厂模式
l先来看看,如果不用反射的时候,的工厂模式吧:
lhttp://www.cnblogs.com/rollenholt/archive/2011/08/18/2144851.html
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39 l40 l41 |
l/** l* @author Rollen-Holt 设计模式之 工厂模式 l*/
linterfacefruit{ lpublicabstractvoideat(); l}
lclassAppleimplementsfruit{ lpublicvoideat(){ lSystem.out.println("Apple"); l} l}
lclassOrangeimplementsfruit{ lpublicvoideat(){ lSystem.out.println("Orange"); l} l}
l// 构造工厂类 l// 也就是说以后如果我们在添加其他的实例的时候只需要修改工厂类就行了 lclassFactory{ lpublicstaticfruit getInstance(String fruitName){ lfruit f=null; lif("Apple".equals(fruitName)){ lf=newApple(); l} lif("Orange".equals(fruitName)){ lf=newOrange(); l} lreturnf; l} l} lclasshello{ lpublicstaticvoidmain(String[] a){ lfruit f=Factory.getInstance("Orange"); lf.eat(); l}
l} |
l这样,当我们在添加一个子类的时候,就需要修改工厂类了。如果我们添加太多的子类的时候,改的就会很多。
l现在我们看看利用反射机制:
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 |
lpackageReflect;
linterfacefruit{ lpublicabstractvoideat(); l}
lclassAppleimplementsfruit{ lpublicvoideat(){ lSystem.out.println("Apple"); l} l}
lclassOrangeimplementsfruit{ lpublicvoideat(){ lSystem.out.println("Orange"); l} l}
lclassFactory{ lpublicstaticfruit getInstance(String ClassName){ lfruit f=null; ltry{ lf=(fruit)Class.forName(ClassName).newInstance(); l}catch(Exception e) { le.printStackTrace(); l} lreturnf; l} l} lclasshello{ lpublicstaticvoidmain(String[] a){ lfruit f=Factory.getInstance("Reflect.Apple"); lif(f!=null){ lf.eat(); l} l} l} |
l现在就算我们添加任意多个子类的时候,工厂类就不需要修改。
l上面的爱吗虽然可以通过反射取得接口的实例,但是需要传入完整的包和类名。而且用户也无法知道一个接口有多少个可以使用的子类,所以我们通过属性文件的形式配置所需要的子类。
l下面我们来看看:结合属性文件的工厂模式
l首先创建一个fruit.properties的资源文件,
l内容为:
l1 l2 |
lapple=Reflect.Apple lorange=Reflect.Orange |
l然后编写主类代码:
l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25 l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39 l40 l41 l42 l43 l44 l45 l46 l47 l48 l49 l50 l51 l52 l53 l54 l55 l56 l57 |
lpackageReflect;
limportjava.io.*; limportjava.util.*;
linterfacefruit{ lpublicabstractvoideat(); l}
lclassAppleimplementsfruit{ lpublicvoideat(){ lSystem.out.println("Apple"); l} l}
lclassOrangeimplementsfruit{ lpublicvoideat(){ lSystem.out.println("Orange"); l} l}
l//操作属性文件类 lclassinit{ lpublicstaticProperties getPro()throwsFileNotFoundException, IOException{ lProperties pro=newProperties(); lFile f=newFile("fruit.properties"); lif(f.exists()){ lpro.load(newFileInputStream(f)); l}else{ lpro.setProperty("apple","Reflect.Apple"); lpro.setProperty("orange","Reflect.Orange"); lpro.store(newFileOutputStream(f),"FRUIT CLASS"); l} lreturnpro; l} l}
lclassFactory{ lpublicstaticfruit getInstance(String ClassName){ lfruit f=null; ltry{ lf=(fruit)Class.forName(ClassName).newInstance(); l}catch(Exception e) { le.printStackTrace(); l} lreturnf; l} l} lclasshello{ lpublicstaticvoidmain(String[] a)throwsFileNotFoundException, IOException{ lProperties pro=init.getPro(); lfruit f=Factory.getInstance(pro.getProperty("apple")); lif(f!=null){ lf.eat(); l} l} l} |
l【运行结果】:Apple
Java 集合框架
前言:数据结构对程序设计有着深远的影响,在面向过程的C语言中,数据库结构用struct来描述,而在面向对象的编程中,数据结构是用类来描述的,并且包含有对该数据结构操作的方法。
在Java语言中,Java语言的设计者对常用的数据结构和算法做了一些规范(接口)和实现(具体实现接口的类)。所有抽象出来的数据结构和操作(算法)统称为Java集合框架(JavaCollectionFramework)。
Java程序员在具体应用时,不必考虑数据结构和算法实现细节,只需要用这些类创建出来一些对象,然后直接应用就可以了,这样就大大提高了编程效率。
1. 先说Set和List:
1.1. Set子接口:无序,不允许重复。List子接口:有序,可以有重复元素。具体区别是
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。<对应类有 HashSet,TreeSet>
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。<相应类有 ArrayList,LinkedList,Vector>
Set和List具体子类:
2.2.<实例比较>
HashSet:以哈希表的形式存放元素,插入删除速度很快。
ArrayList:动态数组,LinkedList:链表、队列、堆栈。
Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用
1.Collection接口
Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些 Collection允许相同的元素而另一些不行。一些能排序而另一些不行。JavaSDK不提供直接继承自Collection的类,JavaSDK提供的类都是继承自Collection的“子接口”如List和Set。
所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection,有一个 Collection参数的构造函数用于创建一个新的Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。
如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:
Iteratorit=collection.iterator();//获得一个迭代子
while(it.hasNext()){
Objectobj=it.next();//得到下一个元素
}
由Collection接口派生的两个接口是List和Set。
2.List接口
List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。
和下面要提到的Set不同,List允许有相同的元素。
除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个 ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素,还能向前或向后遍历。
实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。
2.1.LinkedList类
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在 LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
Listlist=Collections.synchronizedList(newLinkedList(...));
2.2.ArrayList类
ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。
size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。
每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率(自动增判断长度后增长也会浪费时间的呀!)。
和LinkedList一样,ArrayList也是非同步的(unsynchronized)。(扩展阅读:在java.util.concurrent包中定义的CopyOnWriteArrayList提供了线程安全的Arraylist,但是当进行add和set等变化操作时它是通过为底层数组创建新的副本实现的,所以比较耗费资源
(源码在此:publicbooleanadd(E e) {
finalReentrantLocklock =this.lock;
lock.lock();
try{
Object[] elements = getArray();
intlen = elements.length;
Object[] newElements = Arrays.copyOf(elements,len + 1);
newElements[len] = e;
setArray(newElements);
returntrue;
}finally{
lock.unlock();
}
}),
但是如果存在频繁遍历,遍历操作比变化(写入和修改)操作多的时候这种遍历就相对于自己进行的同步遍历效果要好,而且它也允许存在null元素)
2.3.Vector类
Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的 Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。通过使用capacity和ensurecapacity操作以及capacityIncrement域可以优化存储操作,这个前面讲过,(Vector的Iterator和listIterator方法翻译的迭代器支持fail-fast机制,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。官方对此的说明是java.util包中的集合类都返回fail-fast迭代器,这意味着它们假设线程在集合内容中进行迭代时,集合不会更改它的内容。如果fail-fast迭代器检测到在迭代过程中进行了更改操作,那么它会抛出ConcurrentModificationException,这是不可控异常。)
2.4.Stack类
Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。
stack有几个比较实用的方法
boolean |
empty() |
E |
peek() |
E |
pop() |
E |
push(Eitem) |
int |
search(Objecto) |
3.set接口:
Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)Set不保存重复的元素(至于如何判断元素相同则较为负责)
Set : 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。(我变换黄色背景那里的名称得到如下特点)
HashSet : 它不允许出现重复元素;不保证和政集合中元素的顺序,可以自己做个例子可以看出加入的字段顺序跟遍历出的不一样,允许包含值为null的元素,但最多只能有一个null元素(不允许重复嘛!)。
TreeSet : 可以实现排序等功能的集合,它在讲对象元素添加到集合中时会自动按照某种比较规则将其插入到有序的对象序列中,并保证该集合元素组成按照“升序”排列。
a)(在对大量信息进行检索的时候,TreeSet比AraayList更有效率,能保证在log(n)的时间内完成)。
b)TreeSet是实用树形结构来存储信息的,每个节点都会保存一下指针对象,分别指向父节点,左分支,右分支,相比较而言,ArrayList就是一个含有元素的简单数组了,正因为如此,它占的内存也要比ArrayList多一些。
c)想TreeSet插入元素也比ArrayList要快一些,因为当元素插入到ArrayList的任意位置时,平均每次要移动一半的列表,需要O(n)的时间, 而TreeSet深度遍历查询花费的实施只需要O(log(n))(普遍的都是,set查询慢,插入快,list查询快,插入满, .TODO:这一点我会写一个算法测试文章具体分析一下…)
LinkedHashSet : 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。
PS:set有几个比较好的方法:
removeAll(Collection<?> c)
移除 set 中那些包含在指定 collection 中的元素(可选操作)。
boolean retainAll(Collection<?> c)
仅保留 set 中那些包含在指定 collection 中的元素(可选操作)。
containsAll(Collection<?> c)
如果此 set 包含指定 collection 的所有元素,则返回 true。
4.Queue数据结构
这方面知识涉及到线程比较多,有线程基础的口语参考这篇文章
http://blog.csdn.net/a512592151/article/details/38454745
5.Map的功能方法
java为数据结构中的映射定义了一个接口java.util.Map;它有四个实现类,分别是HashMap Hashtable LinkedHashMap和TreeMap
Map主要用于存储健值对,根据键得到值,因此不允许键重复,但允许值重复。
Hashmap 是一个 最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力.
Hashtable 与HashMap类似,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢。
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.在遍历 的时候会比HashMap慢。
TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
package com.hxw.T2;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class MapTester {
public static void init(Map map) {
if (map != null) {
String key = null;
for (int i = 5; i > 0; i--) {
key = new Integer(i).toString() + ".0";
map.put(key, key.toString());
// Map中的键是不重复的,如果插入两个键值一样的记录,
// 那么后插入的记录会覆盖先插入的记录
map.put(key, key.toString() + "0");
}
}
}
public static void output(Map map) {
if (map != null) {
Object key = null;
Object value = null;
// 使用迭代器遍历Map的键,根据键取值
Iterator it = map.keySet().iterator();
while (it.hasNext()) {
key = it.next();
value = map.get(key);
System.out.println("key: " + key + "; value: " + value);
}
// 或者使用迭代器遍历Map的记录Map.Entry
Map.Entry entry = null;
it = map.entrySet().iterator();
while (it.hasNext()) {
// 一个Map.Entry代表一条记录
entry = (Map.Entry) it.next();
// 通过entry可以获得记录的键和值
// System.out.println("key: " + entry.getKey() + "; value: " +
// entry.getValue());
}
}
}
public static boolean containsKey(Map map, Object key) {
if (map != null) {
return map.containsKey(key);
}
return false;
}
public static boolean containsValue(Map map, Object value) {
if (map != null) {
return map.containsValue(value);
}
return false;
}
public static void testHashMap() {
Map myMap = new HashMap();
init(myMap);
// HashMap的键可以为null
myMap.put(null, "ddd");
// HashMap的值可以为null
myMap.put("aaa", null);
output(myMap);
}
public static void testHashtable() {
Map myMap = new Hashtable();
init(myMap);
// Hashtable的键不能为null
// myMap.put(null,"ddd");
// Hashtable的值不能为null
// myMap.put("aaa", null);
output(myMap);
}
public static void testLinkedHashMap() {
Map myMap = new LinkedHashMap();
init(myMap);
// LinkedHashMap的键可以为null
myMap.put(null, "ddd");
// LinkedHashMap的值可以为null
myMap.put("aaa", null);
output(myMap);
}
public static void testTreeMap() {
Map myMap = new TreeMap();
init(myMap);
// TreeMap的键不能为null
// myMap.put(null,"ddd");
// TreeMap的值不能为null
// myMap.put("aaa", null);
output(myMap);
}
public static void main(String[] args) {
System.out.println("采用HashMap");
MapTester.testHashMap();
System.out.println("采用Hashtable");
MapTester.testHashtable();
System.out.println("采用LinkedHashMap");
MapTester.testLinkedHashMap();
System.out.println("采用TreeMap");
MapTester.testTreeMap();
Map myMap = new HashMap();
MapTester.init(myMap);
System.out.println("新初始化一个Map: myMap");
MapTester.output(myMap);
// 清空Map
myMap.clear();
System.out.println("将myMap clear后,myMap空了么? " + myMap.isEmpty());
MapTester.output(myMap);
myMap.put("aaa", "aaaa");
myMap.put("bbb", "bbbb");
// 判断Map是否包含某键或者某值
System.out.println("myMap包含键aaa? "
+ MapTester.containsKey(myMap, "aaa"));
System.out.println("myMap包含值aaaa? "
+ MapTester.containsValue(myMap, "aaaa"));
// 根据键删除Map中的记录
myMap.remove("aaa");
System.out.println("删除键aaa后,myMap包含键aaa? "
+ MapTester.containsKey(myMap, "aaa"));
// 获取Map的记录数
System.out.println("myMap包含的记录数: " + myMap.size());
}
}
附:map 遍历的四种方法:
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第三种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第四种
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
3、其他特征
*List,Set,Map将持有对象一律视为Object型别。
*Collection、List、Set、Map都是接口,不能实例化。
继承自它们的 ArrayList, Vector, HashTable, HashMap是具象class,这些才可被实例化。
*vector容器确切知道它所持有的对象隶属什么型别。vector不进行边界检查。
三、Collections
Collections是针对集合类的一个帮助类。提供了一系列静态方法实现对各种集合的搜索、排序、线程完全化等操作。
相当于对Array进行类似操作的类——Arrays。
如,Collections.max(Collection coll); 取coll中最大的元素。
Collections.sort(List list); 对list中元素排序
四、如何选择?
1、容器类和Array的区别、择取
* 容器类仅能持有对象引用(指向对象的指针),而不是将对象信息copy一份至数列某位置。
* 一旦将对象置入容器内,便损失了该对象的型别信息。
2、
* 在各种Lists中,最好的做法是以ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList();
Vector总是比ArrayList慢,所以要尽量避免使用。
* 在各种Sets中,HashSet通常优于TreeSet(插入、查找)。只有当需要产生一个经过排序的序列,才用TreeSet。
TreeSet存在的唯一理由:能够维护其内元素的排序状态。
* 在各种Maps中
HashMap用于快速查找。
* 当元素个数固定,用Array,因为Array效率是最高的。
结论:最常用的是ArrayList,HashSet,HashMap,Array。而且,我们也会发现一个规律,用TreeXXX都是排序的。
注意:
1、Collection没有get()方法来取得某个元素。只能通过iterator()遍历元素。
2、Set和Collection拥有一模一样的接口。
3、List,可以通过get()方法来一次取出一个元素。使用数字来选择一堆对象中的一个,get(0)...。(add/get)
4、一般使用ArrayList。用LinkedList构造堆栈stack、队列queue。
5、Map用 put(k,v) / get(k),还可以使用containsKey()/containsValue()来检查其中是否含有某个key/value。
HashMap会利用对象的hashCode来快速找到key。
* hashing
哈希码就是将对象的信息经过一些转变形成一个独一无二的int值,这个值存储在一个array中。
我们都知道所有存储结构中,array查找速度是最快的。所以,可以加速查找。
发生碰撞时,让array指向多个values。即,数组每个位置上又生成一个梿表。
6、Map中元素,可以将key序列、value序列单独抽取出来。
使用keySet()抽取key序列,将map中的所有keys生成一个Set。
使用values()抽取value序列,将map中的所有values生成一个Collection。
为什么一个生成Set,一个生成Collection?那是因为,key总是独一无二的,value允许重复。
浅谈JAVA集合框架
Java提供了数种持有对象的方式,包括语言内置的Array,还有就是utilities中提供的容器类(container classes),又称群集类(collection classes)。集合在java中非常重要,在讨论之前,先来看几个面试中的经典问题。
1 Collection 和 Collections的区别。
2 List, Set, Map是否继承自Collection接口。
3 ArrayList和Vector的区别。
4 HashMap和Hashtable的区别。
篇尾有答案,我们开始正题。
集合Collection接口
--Collection 是任何对象组,元素各自独立,通常拥有相同的套用规则。Set List由它派生。
基本操作增加元素add(Object obj); addAll(Collection c);
删除元素 remove(Object obj);removeAll(Collection c);
求交集 retainAll(Collection c);
删除元素 remove(Object obj);removeAll(Collection c);
求交集 retainAll(Collection c);
访问/遍历集合元素的好办法是使用Iterator接口(迭代器用于取代Enumeration)
PublicinterfaceIterator{
PublicBooleanhasNext(};
PublicObjectnext(};
Publicvoidremove(};
}
集set
--没有重复项目的集合
有三种特定类型的集可用
HashSet-基于散列表的集,加进散列表的元素要实现hashCode()方法
LinkedHashSet-对集迭代时,按增加顺序返回元素
TreeSet-基于(平衡)树的数据结构
清单List
--位置性集合。加进清单的元素可以加在清单中特定位置或加到末尾
有两个特定版本
ArrayList(数组表)-类似于Vector,都用于缩放数组维护集合。区别:
一.同步性:Vector是线程安全的,也就是说是同步的,而ArrayList是线程序不安全的,不是同步的
二.数据增长:当需要增长时,Vector默认增长为原来一培,而ArrayList却是原来的一半
LinkedList(链表)-是双向链表,每个节点都有两个指针指向上一节点和下一节点。
用在FIFO,用addList()加入元素 removeFirst()删除元素
用在FILO,用addFirst()/removeLast()
ListIterator提供双向遍历next() previous(),可删除、替换、增加元素
映射表Map
--用于关键字/数值对,像个Dictionary
处理Map的三种集合
关键字集KeySet()
数值集value()
项目集enrySet()
四个具体版本
HashMap-散列表的通用映射表
LinkedHashMap-扩展HashMap,对返回集合迭代时,维护插入顺序
WeakHashMap-基于弱引用散列表的映射表,如果不保持映射表外的关键字的引用,则内存回收程序会回收它
TreeMap-基于平衡树的映射表
HashMap-散列表的通用映射表
LinkedHashMap-扩展HashMap,对返回集合迭代时,维护插入顺序
WeakHashMap-基于弱引用散列表的映射表,如果不保持映射表外的关键字的引用,则内存回收程序会回收它
TreeMap-基于平衡树的映射表
Collections类,用于同步集合,还能改变集合只读方式的类
e.g.:
Mapmp=newHashMap();
mp=Collections.synchronizedMap(mp);//生成线程安全的映射表
mp=Collections.unmodifiableMap(mp);//生成只读映射表
Comparable 自然顺序的排序类Comparator 面向树的集合排序类
容器分类学(Container taxonomy)
集合接口: Collection List Set;Map Iterator ListIterator。
抽象类: AbstractCollection AbstractList AbstractSet AbstractMap AbstractSequentiaList。
老版本中的集合类型
Vector类
Vector,就是向量。一种异构的混合体,可以动态增加容量。对它的操作简要如下
比如我们有一个Vector: Vector myVec=new Vector(a_Array.length)
取得vector的长度:myVec.size();
赋值:set(int position,Object obj) / setElementAt(Object obj, int position) –不支持动态增长
add(Object obj )/ addElement(Object obj) 在Vector末尾加入对象
e.g.:myVec.add(new a_Array[0]);
取出元素:get(int position) / getElement(int position)
Stack类
是Vector的子类。就是数据结构里讲滥了的堆栈(这个词可简称栈,不要混淆于heap-堆)。后进先出的存取方式。
Stack()构造空栈
Empty()叛空
Search()检查堆栈是否有元素
Peek()取得栈顶元素
Pop()弹栈
Push()入栈
Enumeration接口
Dictionary类
字典。关键字/数值方式存取数据,如果映射没有此关键字,取回null。
Hashtable类
是Dictionary结构的具体实现。
面试题答案
Collection 和 Collections的区别。
Collections是个java.util下的类,它包含有各种有关集合操作的静态方法。
Collection是个java.util下的接口,它是各种集合结构的父接口
List, Set, Map是否继承自Collection接口?List,Set是Map不是
ArrayList和Vector的区别。
一.同步性:Vector是线程安全的,也就是说是同步的,而ArrayList是线程序不安全的,不是同步的
二.数据增长:当需要增长时,Vector默认增长为原来一培,而ArrayList却是原来的一半
HashMap和Hashtable的区别
一.历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是Java1.2引进的Map接口的一个实现
二.同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的
三.值:只有HashMap可以让你将空值作为一个表的条目的key或value
一、多线程
1、操作系统有两个容易混淆的概念,进程和线程。
进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。
线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源。
2、Java标准库提供了进程和线程相关的API,进程主要包括表示进程的java.lang.Process类和创建进程的java.lang.ProcessBuilder类;
表示线程的是java.lang.Thread类,在虚拟机启动之后,通常只有Java类的main方法这个普通线程运行,运行时可以创建和启动新的线程;还有一类守护线程(damon thread),守护线程在后台运行,提供程序运行时所需的服务。当虚拟机中运行的所有线程都是守护线程时,虚拟机终止运行。
3、线程间的可见性:一个线程对进程中共享的数据的修改,是否对另一个线程可见
可见性问题:
a、CPU采用时间片轮转等不同算法来对线程进行调度
[java]view plaincopy
1.publicclassIdGenerator{
2.privateintvalue=0;
3.publicintgetNext(){
4.returnvalue++;
5.}
6.}
对于IdGenerator的getNext()方法,在多线程下不能保证返回值是不重复的:各个线程之间相互竞争CPU时间来获取运行机会,CPU切换可能发生在执行间隙。
以上代码getNext()的指令序列:CPU切换可能发生在7条指令之间,多个getNext的指令交织在一起。
[java]view plaincopy
1.aload_0
2.dup
3.getfield#12
4.dup_x1
5.iconst_1
6.iadd
7.putfield#12
b、CPU缓存:
目前CPU一般采用层次结构的多级缓存的架构,有的CPU提供了L1、L2和L3三级缓存。当CPU需要读取主存中某个位置的数据时,会一次检查各级缓存中是否存在对应的数据。如果有,直接从缓存中读取,这比从主存中读取速度快很多。当CPU需要写入时,数据先被写入缓存中,之后再某个时间点写回主存。所以某些时间点上,缓存中的数据与主存中的数据可能是不一致。
c、指令顺序重排
出行性能考虑,编译器在编译时可能会对字节代码的指令顺序进行重新排列,以优化指令的执行顺序,在单线程中不会有问题,但在多线程可能产生与可见性相关的问题。
二、Java内存模型(Java Memory Model)
屏蔽了CPU缓存等细节,只关注主存中的共享变量;关注对象的实例域、静态域和数组元素;关注线程间的动作。
1、volatile关键词:用来对共享变量的访问进行同步,上一次写入操作的结果对下一次读取操作是肯定可见的。(在写入volatile变量值之后,CPU缓存中的内容会被写回内存;在读取volatile变量时,CPU缓存中的对应内容会被置为失效,重新从主存中进行读取),volatile不使用锁,性能优于synchronized关键词。
用来确保对一个变量的修改被正确地传播到其他线程中。
例子:A线程是Worker,一直跑循环,B线程调用setDone(true),A线程即停止任务
[java]view plaincopy
1.publicclassWorker{
2.privatevolatilebooleandone;
3.publicvoidsetDone(booleandone){
4.this.done=done;
5.}
6.publicvoidwork(){
7.while(!done){
8.//执行任务;
9.}
10.}
11.}
例子:错误使用。因为没有锁的支持,volatile的修改不能依赖于当前值,当前值可能在其他线程中被修改。(Worker是直接赋新值与当前值无关)
[java]view plaincopy
1.publicclassCounter{
2.publicvolatilestaticintcount=0;
3.publicstaticvoidinc(){
4.//这里延迟1毫秒,使得结果明显
5.try{
6.Thread.sleep(1);
7.}catch(InterruptedExceptione){
8.}
9.count++;
10.}
11.publicstaticvoidmain(String[]args){
12.//同时启动1000个线程,去进行i++计算,看看实际结果
13.for(inti=0;i<1000;i++){
14.newThread(newRunnable(){
15.@Override
16.publicvoidrun(){
17.Counter.inc();
18.}
19.}).start();
20.}
21.//这里每次运行的值都有可能不同,可能不为1000
22.System.out.println("运行结果:Counter.count="+Counter.count);
23.}
24.}
2、final关键词
final关键词声明的域的值只能被初始化一次,一般在构造方法中初始化。。(在多线程开发中,final域通常用来实现不可变对象)
当对象中的共享变量的值不可能发生变化时,在多线程中也就不需要同步机制来进行处理,故在多线程开发中应尽可能使用不可变对象。
另外,在代码执行时,final域的值可以被保存在寄存器中,而不用从主存中频繁重新读取。
3、java基本类型的原子操作
1)基本类型,引用类型的复制引用是原子操作;(即一条指令完成)
2)long与double的赋值,引用是可以分割的,非原子操作;
3)要在线程间共享long或double的字段时,必须在synchronized中操作,或是声明成volatile
三、Java提供的线程同步方式
1、synchronized关键字
方法或代码块的互斥性来完成实际上的一个原子操作。(方法或代码块在被一个线程调用时,其他线程处于等待状态)
所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。
a、静态方法:Java类对应的Class类的对象所关联的监视器对象。
b、实例方法:当前对象实例所关联的监视器对象。
c、代码块:代码块声明中的对象所关联的监视器对象。
注:当锁被释放,对共享变量的修改会写入主存;当活得锁,CPU缓存中的内容被置为无效。编译器在处理synchronized方法或代码块,不会把其中包含的代码移动到synchronized方法或代码块之外,从而避免了由于代码重排而造成的问题。
例:以下方法getNext()和getNextV2() 都获得了当前实例所关联的监视器对象
[java]view plaincopy
1.publicclassSynchronizedIdGenerator{
2.privateintvalue=0;
3.publicsynchronizedintgetNext(){
4.returnvalue++;
5.}
6.publicintgetNextV2(){
7.synchronized(this){
8.returnvalue++;
9.}
10.}
11.}
2、Object类的wait、notify和notifyAll方法
生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。如果用while 和 volatile也可以做,不过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。
wait: 将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,并且释放B的锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,否则直接抛出java.lang.IllegalMonitorStateException异常)。
notify:将该对象中等待池中的线程,随机选取一个放入对象的锁池,当当前线程结束后释放掉锁, 锁池中的线程即可竞争对象的锁来获得执行机会。
notifyAll:将对象中等待池中的线程,全部放入锁池。
(notify锁唤醒的线程选择由虚拟机实现来决定,不能保证一个对象锁关联的等待集合中的线程按照所期望的顺序被唤醒,很可能一个线程被唤醒之后,发现他所要求的条件并没有满足,而重新进入等待池。因为当等待池中包含多个线程时,一般使用notifyAll方法,不过该方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待池,对性能有影响,不过能保证程序的正确性)
工作流程:
a、Consumer线程A 来 看产品,发现产品为空,调用产品对象的wait(),线程A进入产品对象的等待池并释放产品的锁。
b、Producer线程B获得产品的锁,执行产品的notifyAll(),Consumer线程A从产品的等待池进入锁池,Producer线程B生产产品,然后退出释放锁。
c、Consumer线程A获得产品锁,进入执行,发现有产品,消费产品,然后退出。
例子:
[java]view plaincopy
1.publicsynchronizedStringpop(){
2.this.notifyAll();//唤醒对象等待池中的所有线程,可能唤醒的就是生产者(当生产者发现产品满,就会进入对象的等待池,这里代码省略,基本略同)
3.while(index==-1){//如果发现没产品,就释放锁,进入对象等待池
4.this.wait();
5.}//当生产者生产完后,消费者从this.wait()方法再开始执行,第一次还会执行循环,万一产品还是为空,则再等待,所以这里必须用while循环,不能用if
6.Stringgood=buffer[index];
7.buffer[index]=null;
8.index--;
9.returngood;//消费完产品,退出。
10.}
注:wait()方法有超时和不超时之分,超时的在经过一段时间,线程还在对象的等待池中,那么线程也会推出等待状态。
3、线程状态转换:
已经废弃的方法:stop、suspend、resume、destroy,这些方法在实现上时不安全的。
线程的状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING(有超时的等待)、TERMINATED。
a、方法sleep()进入的阻塞状态,不会释放对象的锁(即大家一起睡,谁也别想执行代码),所以不要让sleep方法处在synchronized方法或代码块中,否则造成其他等待获取锁的线程长时间处于等待。
b、方法join()则是主线程等待子线程完成,再往下执行。例如main方法新建两个线程A和B
[java]view plaincopy
1.publicstaticvoidmain(String[]args)throwsInterruptedException{
2.Threadt1=newThread(newThreadTesterA());
3.Threadt2=newThread(newThreadTesterB());
4.t1.start();
5.t1.join();//等t1执行完再往下执行
6.t2.start();
7.t2.join();//在虚拟机执行中,这句可能被忽略
8.}
c、方法interrupt(),向被调用的对象线程发起中断请求。如线程A通过调用线程B的d的interrupt方法来发出中断请求,线程B来处理这个请求,当然也可以忽略,这不是必须的。Object类的wait()、Thread类的join()和sleep方法都会抛出受检异常java.lang.InterruptedException,通过interrupt方法中断该线程会导致线程离开等待状态。对于wait()调用来说,线程需要重新获取监视器对象上的锁之后才能抛出InterruptedException异常,并致以异常的处理逻辑。
可以通过Thread类的isInterrupted方法来判断是否有中断请求发生,通常可以利用这个方法来判断是否退出线程(类似上面的volatitle修饰符的例子);
Thread类还有个方法Interrupted(),该方法不但可以判断当前线程是否被中断,还会清楚线程内部的中断标记,如果返回true,即曾被请求中断,同时调用完后,清除中断标记。
如果一个线程在某个对象的等待池,那么notify和interrupt 都可以使该线程从等待池中被移除。如果同时发生,那么看实际发生顺序。如果是notify先,那照常唤醒,没影响。如果是interrupt先,并且虚拟机选择让该线程中断,那么即使nofity,也会忽略该线程,而唤醒等待池中的另一个线程。
e、yield(),尝试让出所占有的CPU资源,让其他线程获取运行机会,对操作系统上的调度器来说是一个信号,不一定立即切换线程。(在实际开发中,测试阶段频繁调用yeid方法使线程切换更频繁,从而让一些多线程相关的错误更容易暴露出来)。
四、非阻塞方式
线程之间同步机制的核心是监视对象上的锁,竞争锁来获得执行代码的机会。当一个对象获取对象的锁,然后其他尝试获取锁的对象会处于等待状态,这种锁机制的实现方式很大程度限制了多线程程序的吞吐量和性能(线程阻塞),且会带来死锁(线程A有a对象锁,等着获取b对象锁,线程B有b对象锁,等待获取a对象锁)和优先级倒置(优先级低的线程获得锁,优先级高的只能等待对方释放锁)等问题。
如果能不阻塞线程,又能保证多线程程序的正确性,就能有更好的性能。
在程序中,对共享变量的使用一般遵循一定的模式,即读取、修改和写入三步组成。之前碰到的问题是,这三步执行中可能线程执行切换,造成非原子操作。锁机制是把这三步变成一个原子操作。
目前CPU本身实现 将这三步 合起来 形成一个原子操作,无需线程锁机制干预,常见的指令是“比较和替换”(compare and swap,CAS),这个指令会先比较某个内存地址的当前值是不是指定的旧指,如果是,就用新值替换,否则什么也不做,指令返回的结果是内存地址的当前值。通过CAS指令可以实现不依赖锁机制的非阻塞算法。一般做法是把CAS指令的调用放在一个无限循环中,不断尝试,知道CAS指令成功完成修改。
java.util.concurrent.atomic包中提供了CAS指令。(不是所有CPU都支持CAS,在某些平台,java.util.concurrent.atomic的实现仍然是锁机制)
atomic包中提供的Java类分成三类:
1、支持以原子操作来进行更新的数据类型的Java类(AtomicBoolean、AtomicInteger、AtomicReference),在内存模型相关的语义上,这四个类的对象类似于volatile变量。
类中的常用方法:
a、compareAndSet:接受两个参数,一个是期望的旧值,一个是替换的新值。
b、weakCompareAndSet:效果同compareAndSet(JSR中表示weak原子方式读取和有条件地写入变量但不创建任何 happen-before 排序,但在源代码中和compareAndSet完全一样,所以并没有按JSR实现)
c、get和set:分别用来直接获取和设置变量的值。
d、lazySet:与set类似,但允许编译器把lazySet方法的调用与后面的指令进行重排,因此对值得设置操作有可能被推迟。
例:
[java]view plaincopy
1.publicclassAtomicIdGenerator{
2.privatefinalAtomicIntercounter=newAtomicInteger(0);
3.publicintgetNext(){
4.returncounter.getAndIncrement();
5.}
6.}
7.//getAndIncrement方法的内部实现方式,这也是CAS方法的一般模式,CAS方法不一定成功,所以包装在一个无限循环中,直到成功
8.publicfinalintgetAndIncrement(){
9.for(;;){
10.intcurrent=get();
11.intnext=current+1;
12.if(compareAndSet(current,next))
13.returncurrent;
14.}
15.}
2、提供对数组类型的变量进行处理的Java类,AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray类。(同上,只是放在类数组里,调用时也只是多了一个操作元素索引的参数)
3、通过反射的方式对任何对象中包含的volatitle变量使用CAS方法,AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater。他们提供了一种方式把CAS的功能扩展到了任何Java类中声明为volatitle的域上。(灵活,但语义较弱,因为对象的volatitle可能被非atomic的其他方式被修改)
[java]view plaincopy
1.publicclassTreeNode{
2.privatevolatileTreeNodeparent;
3.//静态工厂方法
4.privatestaticfinalAtomicReferenceFieldUpdater<TreeNode,TreeNode>parentUpdater=AtomicReferenceFieldUpdater.newUpdater(TreeNode.class,TreeNode.class,"parent");
5.publicbooleancompareAndSetParent(TreeNodeexpect,TreeNodeupdate){
6.returnparentUpdater.compareAndSet(this,expect,update);
7.}
8.}
注:java.util.concurrent.atomic包中的Java类属于比较底层的实现,一般作为java.util.concurrent包中很多非阻塞的数据结构的实现基础。
比较多的用AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference。在实现线程安全的计数器时,AtomicInteger和AtomicLong类时最佳的选择。
五、高级同步机制(比synchronized更灵活的加锁机制)
synchronized和volatile,以及wait、notify等方法抽象层次低,在程序开发中使用比较繁琐,易出错。
而多线程之间的交互来说,存在某些固定的模式,如生产者-消费者和读者-写者模式,把这些模式抽象成高层API,使用起来会非常方便。
java.util.concurrent包为多线程提供了高层的API,满足日常开发中的常见需求。
常用接口
1、Lock接口,表示一个锁方法:
a、lock(),获取所,如果无法获取所锁,会处于等待状态
b、unlock(),释放锁。(一般放在finally代码块中)
c、lockInterruptibly(),与lock()类似,但允许当前线程在等待获取锁的过程中被中断。(所以要处理InterruptedException)
d、tryLock(),以非阻塞方式获取锁,如果无法获取锁,则返回false。(tryLock()的另一个重载可以指定超时,如果指定超时,当无法获取锁,会等待而阻塞,同时线程可以被中断)
2、ReadWriteLock接口,表示两个锁,读取的共享锁和写入的排他锁。(适合常见的读者--写者场景)
ReadWriteLock接口的readLock和writeLock方法来获取对应的锁的Lock接口的实现。
在多数线程读取,少数线程写入的情况下,可以提高多线程的性能,提高使用该数据结构的吞吐量。
如果是相反的情况,较多的线程写入,则接口会降低性能。
3、ReentrantLock类和ReentrantReadWriteLock,分别为上面两个接口的实现类。
他们具有重入性:即允许一个线程多次获取同一个锁(他们会记住上次获取锁并且未释放的线程对象,和加锁的次数,getHoldCount())
同一个线程每次获取锁,加锁数+1,每次释放锁,加锁数-1,到0,则该锁被释放,可以被其他线程获取。
[java]view plaincopy
1.publicclassLockIdGenrator{
2.//newReentrantLock(true)是重载,使用更加公平的加锁机制,在锁被释放后,会优先给等待时间最长的线程,避免一些线程长期无法获得锁
3.privateintReentrantLocklock=ReentrantLock();
4.privafteintvalue=0;
5.publicintgetNext(){
6.lock.lock();//进来就加锁,没有锁会等待
7.try{
8.returnvalue++;//实际操作
9.}finally{
10.lock.unlock();//释放锁
11.}
12.}
13.}
注:重入性减少了锁在各个线程之间的等待,例如便利一个HashMap,每次next()之前加锁,之后释放,可以保证一个线程一口气完成便利,而不会每次next()之后释放锁,然后和其他线程竞争,降低了加锁的代价, 提供了程序整体的吞吐量。(即,让一个线程一口气完成任务,再把锁传递给其他线程)。
4、Condition接口,Lock接口代替了synchronized,Condition接口替代了object的wait、nofity。
a、await(),使当前线程进入等待状态,知道被唤醒或中断。重载形式可以指定超时时间。
b、awaitNanos(),以纳秒为单位等待。
c、awaitUntil(),指定超时发生的时间点,而不是经过的时间,参数为java.util.Date。
d、awaitUninterruptibly(),前面几种会响应其他线程发出的中断请求,他会无视,直到被唤醒。
注:与Object类的wait()相同,await()会释放其所持有的锁。
e、signal()和signalAll, 相当于 notify和notifyAll
[java]view plaincopy
1.Locklock=newReentrantLock();
2.Conditioncondition=lock.newCondition();
3.lock.lock();
4.try{
5.while(/*逻辑条件不满足*/){
6.condition.await();
7.}
8.}finally{
9.lock.unlock();
10.}
六、底层同步器
多线程程序中,线程之间存在多种不同的同步方式。除了Java标准库提供的同步方式之外,程序中特有的同步方式需要由开发人员自己来实现。
常见的一种需求是 对有限个共享资源的访问,比如多台个人电脑,2台打印机,当多个线程在等待同一个资源时,从公平角度出发,会用FIFO队列。
如果程序中的同步方式可以抽象成对有限个资源的访问,那么可以使用java.util.concurrent.locks包中的AbstractQueuedSynchronizer类和AbstractQueuedLongSynchronizer类作为实现的基础,前者用int类型的变量来维护内部状态,而后者用long类型。(可以将这个变量理解为共享资源个数)
通过getState、setState、和compareAndSetState3个方法更新内部变量的值。
AbstractQueuedSynchronizer类是abstract的,需要覆盖其中包含的部分方法,通常做法是把其作为一个Java类的内部类,外部类提供具体的同步方式,内部类则作为实现的基础。有两种模式,排他模式和共享模式,分别对应方法 tryAcquire()、tryRelease 和 tryAcquireShared、tryReleaseShared,在这些方法中,使用getState、setState、compareAndSetState3个方法来修改内部变量的值,以此来反应资源的状态。
[java]view plaincopy
1.publicclassSimpleResourceManager{
2.privatefinalInnerSynchronizersynchronizer;
3.privatestaticclassInnerSynchronizerextendsAbstractQueuedSynchronizer{
4.InnerSynchronizer(intnumOfResources){
5.setState(numOfResources);
6.}
7.protectedinttryAcquireShared(intacquires){
8.for(;;){
9.intavailable=getState();
10.intremain=available-acquires;
11.if(remain<0||comapreAndSetState(available,remain){
12.returnremain;
13.}
14.}
15.}
16.protectedbooleantryReleaseShared(intreleases){
17.for(;;){
18.intavailable=getState();
19.intnext=available+releases;
20.if(compareAndSetState(available,next){
21.returntrue;
22.}
23.}
24.}
25.}
26.publicSimpleResourceManager(intnumOfResources){
27.synchronizer=newInnerSynchronizer(numOfResources);
28.}
29.publicvoidacquire()throwsInterruptedException{
30.synchronizer.acquireSharedInterruptibly(1);
31.}
32.pubicvoidrelease(){
33.synchronizer.releaseShared(1);
34.}
35.}
七、高级同步对象(提高开发效率)
atomic和locks包提供的Java类可以满足基本的互斥和同步访问的需求,但这些Java类的抽象层次较低,使用比较复杂。
更简单的做法是使用java.util.concurrent包中的高级同步对象。
1、信号量。
信号量一般用来数量有限的资源,每类资源有一个对象的信号量,信号量的值表示资源的可用数量。
在使用资源时,需要从该信号量上获取许可,成功获取许可,资源的可用数-1;完成对资源的使用,释放许可,资源可用数+1; 当资源数为0时,需要获取资源的线程以阻塞的方式来等待资源,或过段时间之后再来检查资源是否可用。(上面的SimpleResourceManager类实际上时信号量的一个简单实现)
java.util.concurrent.Semaphore类,在创建Semaphore类的对象时指定资源的可用数
a、acquire(),以阻塞方式获取许可
b、tryAcquire(),以非阻塞方式获取许可
c、release(),释放许可。
d、accquireUninterruptibly(),accquire()方法获取许可以的过程可以被中断,如果不希望被中断,使用此方法。
[java]view plaincopy
1.publicclassPrinterManager{
2.privatefinalSemphoresemaphore;
3.privatefinalList<Printer>printers=newArrayList<>():
4.publicPrinterManager(Collection<?extendsPrinter>printers){
5.this.printers.addAll(printers);
6.//这里重载方法,第二个参数为true,以公平竞争模式,防止线程饥饿
7.this.semaphore=newSemaphore(this.printers.size(),true);
8.}
9.publicPrinteracquirePrinter()throwsInterruptedException{
10.semaphore.acquire();
11.returngetAvailablePrinter();
12.}
13.publicvoidreleasePrinter(Printerprinter){
14.putBackPrinter(pinter);
15.semaphore.release();
16.}
17.privatesynchronizedPrintergetAvailablePrinter(){
18.printerresult=printers.get(0);
19.printers.remove(0);
20.returnresult;
21.}
22.privatesynchronizedvoidputBackPrinter(Printerprinter){
23.printers.add(printer);
24.}
25.}
2、倒数闸门
多线程协作时,一个线程等待另外的线程完成任务才能继续进行。
java.util.concurrent.CountDownLatch类,创建该类时,指定等待完成的任务数;当一个任务完成,调用countDonw(),任务数-1。等待任务完成的线程通过await(),进入阻塞状态,直到任务数量为0。CountDownLatch类为一次性,一旦任务数为0,再调用await()不再阻塞当前线程,直接返回。
例:
[java]view plaincopy
1.publicclassPageSizeSorter{
2.//并发性能远远优于HashTable的Map实现,hashTable做任何操作都需要获得锁,同一时间只有有个线程能使用,而ConcurrentHashMap是分段加锁,不同线程访问不同的数据段,完全不受影响,忘记HashTable吧。
3.privatestaticfinalConcurrentHashMap<String,Interger>sizeMap=newConcurrentHashMap<>();
4.privatestaticclassGetSizeWorkerimplementsRunnable{
5.privatefinalStringurlString;
6.publicGetSizeWorker(StringurlString,CountDownLatchsignal){
7.this.urlString=urlStirng;
8.this.signal=signal;
9.}
10.publicvoidrun(){
11.try{
12.InputStreamis=newURL(urlString).openStream();
13.intsize=IOUtils.toByteArray(is).length;
14.sizeMap.put(urlString,size);
15.}catch(IOExceptione){
16.sizeMap.put(urlString,-1);
17.}finally{
18.signal.countDown()://完成一个任务,任务数-1
19.}
20.}
21.}
22.privatevoidsort(){
23.List<Entry<String,Integer>list=newArrayList<sizeMap.entrySet());
24.Collections.slort(list,newComparator<Entry<String,Integer>>(){
25.publicintcompare(Entry<String,Integer>o1,Entry<Sting,Integer>o2){
26.returnInteger.compare(o2.getValue(),o1.getValue());
27.};
28.System.out.println(Arrays.deepToString(list.toArray()));
29.}
30.publicvoidsortPageSize(Collection<String>urls)throwsInterruptedException{
31.CountDownLatchsortSignal=newCountDownLatch(urls.size());
32.for(Stringurl:urls){
33.newThread(newGetSizeWorker(url,sortSignal)).start();
34.}
35.sortSignal.await()://主线程在这里等待,任务数归0,则继续执行
36.sort();
37.}
38.}
3、循环屏障
循环屏障在作用上类似倒数闸门,不过他不像倒数闸门是一次性的,可以循环使用。另外,线程之间是互相平等的,彼此都需要等待对方完成,当一个线程完成自己的任务之后,等待其他线程完成。当所有线程都完成任务之后,所有线程才可以继续运行。
当线程之间需要再次进行互相等待时,可以复用同一个循环屏障。
类java.uti.concurrent.CyclicBarrier用来表示循环屏障,创建时指定使用该对象的线程数目,还可以指定一个Runnable接口的对象作为每次循环后执行的动作。(当最后一个线程完成任务之后,所有线程继续执行之前,被执行。如果线程之间需要更新一些共享的内部状态,可以利用这个Runnalbe接口的对象来处理)。
每个线程任务完成之后,通过调用await方法进行等待,当所有线程都调用await方法之后,处于等待状态的线程都可以继续执行。在所有线程中,只要有一个在等待中被中断,超时或是其他错误,整个循环屏障会失败,所有等待中的其他线程抛出java.uti.concurrent.BrokenBarrierException。
例:每个线程负责找一个数字区间的质数,当所有线程完成后,如果质数数目不够,继续扩大范围查找
[java]view plaincopy
1.publicclassPrimeNumber{
2.privatestaticfinalintTOTAL_COUTN=5000;
3.privatestaticfinalintRANGE_LENGTH=200;
4.privatestaticfinalintWORKER_NUMBER=5;
5.privatestaticvolatitlebooleandone=false;
6.privatestaticintrangeCount=0;
7.privatestaticfinalList<Long>results=newArrayList<Long>():
8.privatestaticfinalCyclicBarrierbarrier=newCyclicBarrier(WORKER_NUMBER,newRunnable(){
9.publicvoidrun(){
10.if(results.size()>=TOTAL_COUNT){
11.done=true;
12.}
13.}
14.});
15.privatestaticclassPrimeFinderimplementsRunnable{
16.publicvoidrun(){
17.while(!done){//整个过程在一个while循环下,await()等待,下次循环开始,会再次判断执行条件
18.intrange=getNextRange();
19.longstart=rang*RANGE_LENGTH;
20.longend=(range+1)*RANGE_LENGTH;
21.for(longi=start;i<end;i++){
22.if(isPrime(i)){
23.updateResult(i);
24.}
25.}
26.try{
27.barrier.await();
28.}catch(InterruptedException|BokenBarrierExceptione){
29.done=true;
30.}
31.}
32.}
33.}
34.privatesynchronizedstaticvoidupdateResult(longvalue){
35.results.add(value);
36.}
37.privatesynchronizedstaticintgetNextRange(){
38.returnrangeCount++;
39.}
40.privatestaticbooleanisPrime(longnumber){
41.//找质数的代码
42.}
43.publicvoidcalculate(){
44.for(inti=0;i<WORKER_NUMBER;i++){
45.newThread(newPrimeFinder()).start();
46.}
47.while(!done){
48.
49.}
50.//计算完成
51.}
52.}
4、对象交换器
适合于两个线程需要进行数据交换的场景。(一个线程完成后,把结果交给另一个线程继续处理)
java.util.concurrent.Exchanger类,提供了这种对象交换能力,两个线程共享一个Exchanger类的对象,一个线程完成对数据的处理之后,调用Exchanger类的exchange()方法把处理之后的数据作为参数发送给另外一个线程。而exchange方法的返回结果是另外一个线程锁提供的相同类型的对象。如果另外一个线程未完成对数据的处理,那么exchange()会使当前线程进入等待状态,直到另外一个线程也调用了exchange方法来进行数据交换。
例:
[java]view plaincopy
1.publicclassSendAndReceiver{
2.privatefinalExchanger<StringBuilder>exchanger=newExchanger<StringBuilder>();
3.privateclassSenderimplementsRunnable{
4.publicvoidrun(){
5.try{
6.StringBuildercontent=newStringBuilder("Hello");
7.content=exchanger.exchange(content);
8.}catch(InterruptedExceptione){
9.Thread.currentThread().interrupt();
10.}
11.}
12.}
13.privateclassReceiverimplementsRunnable{
14.publicvoidrun(){
15.try{
16.StringBuildercontent=newStringBuilder("World");
17.content=exchanger.exchange(content);
18.}catch(InterruptedExceptione){
19.Thread.currentThread().interrupt();
20.}
21.}
22.}
23.publicvoidexchange(){
24.newThread(newSender()).start();
25.newThread(newReceiver()).start();
26.}
27.}
八、数据结构(多线程程序使用的高性能数据结构)
java.util.concurrent包中提供了一些适合多线程程序使用的高性能数据结构,包括队列和集合类对象等。
1、队列
a、BlockingQueue接口:线程安全的阻塞式队列;当队列已满时,想队列添加会阻塞;当队列空时,取数据会阻塞。(非常适合消费者-生产者模式)
阻塞方式:put()、take()。
非阻塞方式:offer()、poll()。
实现类:基于数组的固定元素个数的ArrayBolockingQueue和基于链表结构的不固定元素个数的LinkedBlockQueue类。
b、BlockingDeque接口: 与BlockingQueue相似,但可以对头尾进行添加和删除操作的双向队列;方法分为两类,分别在队首和对尾进行操作。
实现类:标准库值提供了一个基于链表的实现,LinkedBlockgingDeque。
2、集合类
在多线程程序中,如果共享变量时集合类的对象,则不适合直接使用java.util包中的集合类。这些类要么不是线程安全,要么在多线程下性能比较差。
应该使用java.util.concurrent包中的集合类。
a、ConcurrentMap接口: 继承自java.util.Map接口
putIfAbsent():只有在散列表不包含给定键时,才会把给定的值放入。
remove():删除条目。
replace(key,value):把value 替换到给定的key上。
replace(key, oldvalue, newvalue):CAS的实现。
实现类:ConcurrentHashMap:
创建时,如果可以预估可能包含的条目个数,可以优化性能。(因为动态调整所能包含的数目操作比较耗时,这个HashMap也一样,只是多线程下更耗时)。
创建时,预估进行更新操作的线程数,这样实现中会根据这个数把内部空间划分为对应数量的部分。(默认是16,如果只有一个线程进行写操作,其他都是读取,那么把值设为1 可以提高性能)。
注:当从集合中创建出迭代器遍历Map元素时,不一定能看到正在添加的数据,只能和集合保证弱一致性。(当然使用迭代器不会因为查看正在改变的Map,而抛出java.util.ConcurrentModifycationException)
b、CopyOnWriteArrayList接口:继承自java.util.List接口。
顾名思义,在CopyOnWriteArrayList的实现类,所有对列表的更新操作都会新创建一个底层数组的副本,并使用副本来存储数据;对列表更新操作加锁,读取操作不加锁。
适合多读取少修改的场景,如果更新操作多,那么不适合用,同样迭代器只能表示创建时列表的状态,更新后使用了新的底层数组,迭代器还是引用旧的底层数组。
九、多线程任务的执行
过去线程的执行,是先创建Thread类的想,再调用start方法启动,这种做法要求开发人员对线程进行维护,在线程较多时,一般创建一个线程池同一管理,同时降低重复创建线程的开销
在J2SE5.0中,java.util.concurrent包提供了丰富的用来管理线程和执行任务的实现。
1、基本接口(描述任务)
a、Callable接口:
Runnable接口受限于run方法的类型签名,而Callable只有一个方法call(),可以有返回值,可以抛出受检异常。
b、Future接口:
过去,需要异步线程的任务执行结果,要求主线程和任务执行线程之间进行同步和数据传递。
Future简化了任务的异步执行,作为异步操作的一个抽象。调用get()方法可以获取异步的执行结果,如果任务没有执行完,会等待,直到任务完成或被取消,cancel()可以取消。
c、Delayed接口:
延迟执行任务,getDelay()返回当前剩余的延迟时间,如果不大于0,说明延迟时间已经过去,应该调度并执行该任务。
2、组合接口(描述任务)
a、RunnableFuture接口:继承自Runnable接口和Future接口。
当来自Runnalbe接口中的run方法成功执行之后,相当于Future接口表示的异步任务已经完成,可以通过get()获取运行结果。
b、ScheduledFuture接口:继承Future接口和Delayed接口,表示一个可以调用的异步操作。
c、RunnableScheduledFuture接口:继承自Runnable、Delayed和Future,接口中包含isPeriodic,表明该异步操作是否可以被重复执行。
3、Executor接口、ExcutorServer接口、ScheduleExecutorService接口和CompletionService接口(描述任务执行)
a、executor接口,execute()用来执行一个Runnable接口的实现对象,不同的Executor实现采取不同执行策略,但提供的任务执行功能比较弱。
b、excutorServer接口,继承自executor;
提供了对任务的管理:submit(),可以吧Callable和Runnable作为任务提交,得到一个Future作为返回,可以获取任务结果或取消任务。
提供批量执行:invokeAll()和invokeAny(),同时提交多个Callable;invokeAll(),会等待所有任务都执行完成,返回一个包含每个任务对应Future的列表;invokeAny(),任何一个任务成功完成,即返回该任务结果。
提供任务关闭:shutdown()、shutdownNow()来关闭服务,前者不允许新的任务提交,后者试图终止正在运行和等待的任务,并返回已经提交单没有被运行的任务列表。(两个方法都不会等待服务真正关闭,只是发出关闭请求。)。shutdownDow,通常做法是向线程发出中断请求,所以确保提交的任务实现了正确的中断处理逻辑。
c、ScheduleExecutorService接口,继承自excutorServer接口:支持任务的延迟执行和定期执行,可以执行Callable或Runnable。
schedule(),调度一个任务在延迟若干时间之后执行;
scheduleAtFixedRate():在初始延迟后,每隔一段时间循环执行;在下一次执行开始时,上一次执行可能还未结束。(同一时间,可能有多个)
scheduleWithFixedDelay:同上,只是在上一次任务执行完后,经过给定的间隔时间再开始下一次执行。(同一时间,只有一个)
以上三个方法都返回ScheduledFuture接口的实现对象。
d、CompletionService接口,共享任务执行结果。
通常在使用ExecutorService接口,通过submit提交任务,并得到一个Future接口来获取任务结果,如果任务提交者和执行结果的使用者是程序的不同部分,那就要把Future在不同部分进行传递;而CompletionService就是解决这个问题,程序不同部分可以共享CompletionService,任务提交后,执行结果可以通过take(阻塞),poll(非阻塞)来获取。
标准库提供的实现是 ExecutorCompletionService,在创建时,需要提供一个Executor接口的实现作为参数,用来实际执行任务。
例:多线程方式下载文件
[java]view plaincopy
1.publicclassFileDownloader{
2.//线程池
3.privatefinalExecutorServiceexecutor=Executors.newFixedThreadPool(10);
4.publicbooleandownload(finalURLurl,finalPathpath){
5.Future<Path>future=executor.submit(newCallable<Path>(){//submit提交任务
6.publicPathcall(){
7.//这里就省略IOException的处理了
8.InputStreamis=url.openStream();
9.Files.copy(is,path,StandardCopyOption.REPLACE_EXISTING);
10.returnpath;
11.});
12.try{
13.returnfuture.get()!=null?true:false;
14.}<spanstyle="font-family:Arial,Helvetica,sans-serif;">catch(InterruptedException|ExecutionExceptione){</span>
15.returnfalse;
16.}
17.}
18.publicvoidclose(){//当不再使用FileDownloader类的对象时,应该使用close方法关闭其中包含的ExecutorService接口的实现对象,否则虚拟机不会退出,占用内存不释放
19.executor.shutdown();//发出关闭请求,此时不会再接受新任务
20.try{
21.if(!executor.awaitTermination(3,TimeUnit.MINUTES)){//awaitTermination来等待一段时间,使正在执行的任务或等待的任务有机会完成
22.executor.shutdownNow();//如果等待时间过后还有任务没完成,则强制结束
23.executor.awaitTermination(1,TimeUnit.MINUTES);//再等待一段时间,使被强制结束的任务完成必要的清理工作
24.}
25.}catch(InterruptedExceptione){
26.executor.shutdownNow();
27.Thread.currentThread().interrupt();
28.}
29.}
30.}
十、Java SE7 新特性
对java.util.concurrent包进行更新,增加了新的轻量级任务执行框架fork/join和多阶段线程同步工具。
1、轻量级任务执行框架fork/join
这个框架的目的主要是更好地利用底层平台上的多核和多处理器来进行并行处理。
通过分治算法或map/reduce算法来解决问题。
fork/join 类比于 map/reduce。
fork操作是把一个大的问题划分为若干个较小的问题,划分过程一般为递归,直到可以直接进行计算的粒度适合的子问题;子问题在结算后,可以得到整个问题的部分解
join操作收集子结果,合并,得到完整解,也可能是 递归进行的。
相对一般的线程池实现,F/J框架的优势在任务的处理方式上。在一般线程池中,一个线程由于某些原因无法运行,会等待;而在F/J,某个子问题由于等待另外一个子问题的完成而无法继续运行,那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行。这种方式减少了等待时间,提高了性能。
为了F/J能高效,在每个子问题视线中应避免使用synchronized或其他方式进行同步,也不应使用阻塞式IO或过多访问共享变量。在理想情况下,每个子问题都应值进行CPU计算,只使用每个问题的内部对象,唯一的同步应只发生在子问题和创建它的父问题之间。(这完全就是Hadoop的MapReduce嘛)
a、ForkJoinTask类:表示一个由F/J框架执行的任务,该类实现了Future接口,可以按照Future接口的方式来使用。(表示任务)
fork(),异步方式启动任务的执行。
join(),等待任务完成并返回执行结果。
在创建自己的任务时,最好不要直接继承自ForkJoinTask,而是继承其子类,RecuriveTask或RecursiveAction,前者可以返回结果,后者不行。
b、ForkJoinPool类:表示任务执行,实现了ExecutorService接口,除了可以执行ForkJoinTask,也可以执行Callable和Runnable。(任务执行)
执行任务的两大类:
第一类:execute、invoke或submit方法:直接提交任务。
第二类:fork():运行ForkJoinTask在执行过程中的子任务。
一般作法是表示整个问题的ForkJoinTask用第一类提交,执行过程中产生的子任务不需要处理,ForkJoinPool会负责子任务执行。
例:查找数组中的最大值
[java]view plaincopy
1.privatestaticclassMaxValueTaskextendsRecursiveTask<Long>{
2.privatefinallong[]array;
3.privatefinalintstart;
4.privatefinalintend;
5.MaxValueTask(long[]array,intstart,intend){
6.this.array=array;
7.this.start=start;
8.this.end=end;
9.}
10.//compute是RecursiveTask的主方法
11.protectedlongcompute(){
12.longmax=Long.MIN_VALUE;
13.if(end-start<RANG_LENGTH){//寻找最大值
14.for(inti=start;i<end;i++{
15.if(array[i]>max){
16.max=array[i];
17.}
18.}
19.}else{//二分任务
20.intmid=(start+end)/2;
21.MaxValueTasklowTask=newMaxValueTask(array,start,mid);
22.MaxValueTaskhighTask=newMaxValueTask(array,mid,end);
23.lowTask.fork();//异步启动任务
24.highTask.fork();
25.max=Math.max(max,lowTask.join());//等待执行结果
26.max=Math.max(max,highTask.join();
27.}
28.returnmax;
29.}
30.publicLongcalculate(long[]array){
31.MaxValueTasktask=newMaxValueTask(array,0,array.length);
32.Longresult=forkJoinPool.invoke(task);
33.returnresult;
34.}
35.}
注:这个例子是示例,但从性能上说直接对整个数组顺序比较效率高,毕竟多线程所带来的额外开销过大。
在实际中,F/J框架发挥作用的场合很多,比如在一个目录包含的所有文本中搜索某个关键字,可以每个文件创建一个子任务。
如果相关的功能可以用递归和分治来解决,就适合F/J。
2、多阶段线程同步工具
Phaser类是Java SE 7中新增的一个使用同步工具,功能和灵活性比倒数闸门和循环屏障要强很多。
在F/J框架中的子任务之间要进行同步时,应优先考虑Phaser。
Phaser把多个线程写作执行的任务划分成多个阶段(phase),编程时要明确各个阶段的任务,每个阶段都可以有任意个参与者,线程可以随时注册并参与到某个阶段,当一个阶段中所有线程都成功完成之后,Phaser的onAdvance()被调用,可以通过覆盖添加自定义处理逻辑(类似循环屏障的使用的Runnable接口),然后Phaser类会自动进入下个阶段。如此循环,知道Phaser不再包含任何参与者。
Phaser创建后,初始阶段编号为0,构造函数中指定初始参与个数。
register(),bulkRegister(),动态添加一个或多个参与者。
arrive(),某个参与者完成任务后调用
arriveAndDeregister(),任务完成,取消自己的注册。
arriveAndAwaitAdvance(),自己完成等待其他参与者完成。,进入阻塞,直到Phaser成功进入下个阶段。
awaitAdvance()、awaitAdvanceInterruptibly(),等待phaser进入下个阶段,参数为当前阶段的编号,后者可以设置超时和处理中断请求。
另外,Phaser的一个重要特征是多个Phaser可以组成树形结构,Phaser提供了构造方法来指定当前对象的父对象;当一个子对象参与者>0,会自动注册到父对象中;当=0,自动解除注册。
例:从指定网址,下载img标签的照片
阶段1、处理网址对应的html文本,和抽取img的链接;2、创建图片下载子线程,主线程等待;3、子线程下载图片,主线程等待;4、任务完成退出
[java]view plaincopy
1.publicclassWebPageImageDownloader{
2.privatefinalPhaserphaser=newPhaser(1);//初始参与数1,代表主线程。
3.publicvoiddownload(URLurl,finalPathpath)throwsIOException{
4.Stringcontent=getContent(url);//获得HTML文本,省略。
5.List<URL>imageUrls=extractImageUrls(content);//获得图片链接,省略。
6.for(finalURLimageUrl:imageUrls){
7.phaser.register();//子线程注册
8.newThread(){
9.publicvoidrun(){
10.phaser.arriveAndAwaitAdvance();//第二阶段的等待,等待进入第三阶段
11.try{
12.InputStreamis=imageUrl.openStream();
13.File.copy(is,getSavePath(path,imageUrl),StandardCopyOption.REPLACE_EXISTING);
14.}catch(IOExceptione){
15.e.printStackTrace():
16.}finally{
17.phaser.arriveAndDeregister();//子线程完成任务,退出。
18.}
19.}
20.}.start();
21.}
22.phaser.arriveAndAwaitAdvance();//第二阶段等待,子线程在注册
23.phaser.arriveAndAwaitAdvance();//第三阶段等待,子线程在下载
24.phaser.arriveAndDeregister();//所有线程退出。
25.}
26.}
十一、ThreadLocal类
java.lang.ThreadLocal,线程局部变量,把一个共享变量变为一个线程的私有对象。不同线程访问一个ThreadLocal类的对象时,锁访问和修改的事每个线程变量各自独立的对象。通过ThreadLocal可以快速把一个非线程安全的对象转换成线程安全的对象。(同时也就不能达到数据传递的作用了)。
a、get()和set()分别用来获取和设置当前线程中包含的对象的值。
b、remove(),删除。
c、initialValue(),初始化值。如果没有通过set方法设置值,第一个调用get,会通过initValue来获取对象的初始值。
ThreadLoacl的一般用法,创建一个ThreadLocal的匿名子类并覆盖initalValue(),把ThreadLoacl的使用封装在另一个类中
[java]view plaincopy
1.publicclassThreadLocalIdGenerator{
2.privatestaticfinalThreadLocal<IdGenerator>idGenerator=newThreadLocal<IdGenerator>(){
3.protectedIdGeneratorinitalValue(){
4.returnnewIdGenerator();//IdGenerator是个初始intvalue=0,然后getNext(){returnvalue++}
5.}
6.};
7.publicstaticintgetNext(){
8.returnidGenerator.get().getNext();
9.}
10.}
ThreadLoal的另外一个作用是创建线程唯一的对象,在有些情况,一个对象在代码中各个部分都需要用到,传统做法是把这个对象作为参数在代码间传递,如果使用这个对I昂的代码都在同一个线程,可以封装在ThreadLocal中。
如:在多线程中,生成随机数
java.util.Random会带来竞争问题,java.util.concurrent.ThreadLocalRandom类提供多线程下的随机数声场,底层是ThreadLoacl。
总结:多线程开发中应该优先使用高层API,如果无法满足,使用java.util.concurrent.atomic和java.util.concurrent.locks包提供的中层API,而synchronized和volatile,以及wait,notify和notifyAll等低层API 应该最后考虑。
java多线程基本入门(代码)
java多线程编程还是比较重要的,在实际业务开发中经常要遇到这个问题。 java多线程,传统创建线程的方式有两种。 1、继承自Thread类,覆写run方法。 2、实现Runnable接口,实现run方法。 启动线程的方法都是调用start方法,真正执行调用的是run方法。
参考代码如下:
复制代码代码如下:
package com.jack.thread;
/**
* 线程简单演示例子程序
*
* @author pinefantasy
* @since 2013-10-31
*/
public class ThreadDemo1 {
/**
* 第一种方式:继承自Thread类,覆写run方法
*/
public static class Test1Thread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Test1," + Thread.currentThread().getName() + ", i = " + i);
}
}
}
/**
* 第二种方式:实现Runnable接口,实现run方法
*/
public static class Test2Thread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Test2," + Thread.currentThread().getName() + ", i = " + i);
}
}
}
/**
* <pre>
*
* 主线程为main线程
* 分支线程为:1 2 3 三种简单实现方式
*
* @param args
*/
public static void main(String[] args) {
new Test1Thread().start();// 启动线程1
new Thread(new Test2Thread()).start();// 启动线程2
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Test3," + Thread.currentThread().getName() + ", i = " + i);
}
}
}).start();// 启动线程3
}
}
二、java并发包简单入门
多个线程,统一处理同一个变量演示代码:
复制代码代码如下:
package com.jack.thread;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 多线程对同一个变量进行操作
*
* @author pinefantasy
* @since 2013-10-31
*/
public class ThreadDemo2 {
private static int count = 0;
public static class CountThread implements Runnable {// 1.这边有线程安全问题,共享变量乱套了
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
System.out.println(Thread.currentThread().getName() + ", count = " + count);
}
}
}
private static final Object lock = new Object();// 这边使用的lock对象
public static class Count2Thread implements Runnable {// 这边使用的是互斥锁方式
@Override
public void run() {
synchronized (lock) {// 使用互斥锁方式处理
for (int i = 0; i < 100; i++) {
count++;
System.out.println(Thread.currentThread().getName() + ", count = " + count);
}
}
}
}
private static AtomicInteger ai = new AtomicInteger();// 这边使用的是并发包的AtomicXXX类,使用的是CAS方式:compare and swap
public static class Count3Thread implements Runnable {// AtomicInteger内部的CAS实现方式,采用的是:循环、判断、设置三部曲方式
@Override
public void run() {
for (int i = 0; i < 100; i++) {
int tmp = ai.incrementAndGet();// 采用CAS方式处理
System.out.println(Thread.currentThread().getName() + ", count = " + tmp);
}
}
}
private static volatile int countV = 0;// 定义成volatile,让多线程感知,因为值是放在主存中
public static class Count4Thread implements Runnable {// volatile定义的变量只是说放到了主存,当时++操作并不是原子操作,这个要小心
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(50);// 这边让线程休眠下,增加出错概率
} catch (InterruptedException e) {
e.printStackTrace();
}
countV++;// volatile要正确使用,不是说定义成volatile就是安全的,还是要注意++ --操作并不是原子操作
System.out.println(Thread.currentThread().getName() + ", count = " + countV);
}
}
}
/**
* 使用泛型简单编写一个测试方法
*
* @param <T>
* @param t
* @throws InstantiationException
* @throws IllegalAccessException
* @throws InterruptedException
*/
public static <T> void testTemplate(T t) throws InstantiationException, IllegalAccessException, InterruptedException {
for (int i = 0; i < 5; i++) {
if (t instanceof Runnable) {
Class<?> c = t.getClass();
Object object = c.newInstance();
new Thread((Runnable) object).start();
}
}
}
/**
* <pre>
* 1.test1 线程不安全演示例子,count变量不能得到预期的效果
* 2.test2 在test1基础上改进的,用互斥锁sync处理
* 3.test3 在test1基础上改进的,用AtomicInteger类来实现
* 4.test4 有问题的方法,因为i++并不是原子操作,将count定义为volatile类型的
*
* @param args
* @throws InterruptedException
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static void main(String[] args) throws InterruptedException, InstantiationException, IllegalAccessException {
// 1.测试1
// testTemplate(new CountThread());
// 2.测试2
// testTemplate(new Count2Thread());
// 3.测试3
// testTemplate(new Count3Thread());
// 4.测试4
testTemplate(new Count4Thread());
Thread.sleep(15000);
System.out.println(count);
System.out.println(ai.get());
System.out.println(countV);
}
}
生产者-消费者模式
生产者(生成产品的线程)--》负责生成产品 消费者(消费产品的线程)--》负责消费产品
买车人、消费者。 卖车人、销售汽车的人、姑且当做生产者。 仓库、存放汽车的地方。 汽车工厂、真实生成汽车的地方。
参考代码如下:
// 没有加上同步机制的代码如下:
复制代码代码如下:
package com.jack.thread;
import java.util.ArrayList;
import java.util.List;
import com.jack.thread.ThreadDemo3.CarBigHouse.Car;
/**
* 第一个版本的生产者和消费者线程
*
* @author pinefantasy
* @since 2013-11-1
*/
public class ThreadDemo3 {
/**
* 姑且卖车的当做是生产者线程
*/
public static class CarSeller implements Runnable {
private CarBigHouse bigHouse;
public CarSeller(CarBigHouse bigHouse) {
this.bigHouse = bigHouse;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {// 当做生产者线程,往仓库里边增加汽车,其实是触发增加汽车
int count = bigHouse.put();
System.out.println("生产汽车-->count = " + count);
}
}
}
/**
* 姑且买车的人当做是消费者线程
*/
public static class Consumer implements Runnable {
private CarBigHouse bigHouse;
public Consumer(CarBigHouse bigHouse) {
this.bigHouse = bigHouse;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {// 当做消费者线程,从仓库里边提取汽车,其实是触发,从仓库里边提取一辆汽车出来
int count = bigHouse.get();
System.out.println("消费汽车-->count = " + count);
}
}
}
/**
* 这边姑且当做是车子big house放车子的仓库房
*/
public static class CarBigHouse {
public int carNums = 0;// 这边是仓库房子中车子的数量总数
public List<Car> carList = new ArrayList<Car>();// 这边模拟用来放汽车的list
public int put() {// 提供给生产者放汽车到仓库的接口
Car car = CarFactory.makeNewCar();
carList.add(car);// 加到仓库中去
carNums++;// 总数增加1
return carNums;
}
public int get() {// 提供给消费者从这边取汽车接口
Car car = null;
if (carList.size() != 0) {// size不为空才去取车
car = carList.get(carList.size() - 1);// 提取最后一个car
carList.remove(car);// 从从库list中移除掉
carNums--;// 总数减少1
}
return carNums;
}
public static class Car {
public String carName;// 汽车名称
public double carPrice;// 汽车价格
public Car() {
}
public Car(String carName, double carPrice) {
this.carName = carName;
this.carPrice = carPrice;
}
}
}
/**
* 采用静态工厂方式创建car对象,这个只是简单模拟,不做设计模式上的过多考究
*/
public static class CarFactory {
private CarFactory() {
}
public static Car makeNewCar(String carName, double carPrice) {
return new Car(carName, carPrice);
}
public static Car makeNewCar() {
return new Car();
}
}
/**
* 第一个版本的生产者和消费者线程,没有加上同步机制的演示例子
*
* @param args
*/
public static void main(String[] args) {
CarBigHouse bigHouse = new CarBigHouse();
new Thread(new CarSeller(bigHouse)).start();
new Thread(new Consumer(bigHouse)).start();
}
}
// 加上互斥锁的代码如下:
复制代码代码如下:
package com.jack.thread;
import java.util.ArrayList;
import java.util.List;
import com.jack.thread.ThreadDemo4.CarBigHouse.Car;
/**
* 第二个版本的生产者消费者线程
*
* @author pinefantasy
* @since 2013-11-1
*/
public class ThreadDemo4 {
/**
* 姑且卖车的当做是生产者线程
*/
public static class CarSeller implements Runnable {
private CarBigHouse bigHouse;
public CarSeller(CarBigHouse bigHouse) {
this.bigHouse = bigHouse;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {// 当做生产者线程,往仓库里边增加汽车,其实是触发增加汽车
int count = bigHouse.put();
System.out.println("生产汽车-->count = " + count);
}
}
}
/**
* 姑且买车的人当做是消费者线程
*/
public static class Consumer implements Runnable {
private CarBigHouse bigHouse;
public Consumer(CarBigHouse bigHouse) {
this.bigHouse = bigHouse;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {// 当做消费者线程,从仓库里边提取汽车,其实是触发,从仓库里边提取一辆汽车出来
int count = bigHouse.get();
System.out.println("消费汽车-->count = " + count);
}
}
}
/**
* 这边姑且当做是车子big house放车子的仓库房
*/
public static class CarBigHouse {
public int carNums = 0;// 这边是仓库房子中车子的数量总数
public List<Car> carList = new ArrayList<Car>();// 这边模拟用来放汽车的list
// 直接增加上synchronized关键字方式,成员方法,锁的是当前bigHouse对象
// 这种锁是互斥锁,方法在同一个时刻,只有一个线程可以访问到里边的代码
public synchronized int put() {// 提供给生产者放汽车到仓库的接口
Car car = CarFactory.makeNewCar();
carList.add(car);// 加到仓库中去
carNums++;// 总数增加1
return carNums;
}
public synchronized int get() {// 提供给消费者从这边取汽车接口
Car car = null;
if (carList.size() != 0) {// size不为空才去取车
car = carList.get(carList.size() - 1);// 提取最后一个car
carList.remove(car);// 从从库list中移除掉
carNums--;// 总数减少1
}
return carNums;
}
public static class Car {
public String carName;// 汽车名称
public double carPrice;// 汽车价格
public Car() {
}
public Car(String carName, double carPrice) {
this.carName = carName;
this.carPrice = carPrice;
}
}
}
/**
* 采用静态工厂方式创建car对象,这个只是简单模拟,不做设计模式上的过多考究
*/
public static class CarFactory {
private CarFactory() {
}
public static Car makeNewCar(String carName, double carPrice) {
return new Car(carName, carPrice);
}
public static Car makeNewCar() {
return new Car();
}
}
/**
* 第二个版本的生产者和消费者线程,加上了同步机制的方法
*
* @param args
*/
public static void main(String[] args) {
CarBigHouse bigHouse = new CarBigHouse();
new Thread(new CarSeller(bigHouse)).start();
new Thread(new Consumer(bigHouse)).start();
}
}
/ 采用Object类的wait和notify方法或者notifyAll方法(注意notify方法和notifyAll方法区别) // notify是唤醒其中一个在等待的线程。 // notifyAll是唤醒其他全部在等待的线程,但是至于哪个线程可以获得到锁还是要看竞争关系。
线程状态:创建、运行、阻塞、销毁状态。(阻塞情况比较多,比如等待数据IO输入,阻塞了。)
复制代码代码如下:
package com.jack.thread;
import java.util.ArrayList;
import java.util.List;
import com.jack.thread.ThreadDemo4.CarBigHouse.Car;
/**
* 第二个版本的生产者消费者线程
*
* @author pinefantasy
* @since 2013-11-1
*/
public class ThreadDemo4 {
/**
* 姑且卖车的当做是生产者线程
*/
public static class CarSeller implements Runnable {
private CarBigHouse bigHouse;
public CarSeller(CarBigHouse bigHouse) {
this.bigHouse = bigHouse;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {// 当做生产者线程,往仓库里边增加汽车,其实是触发增加汽车
int count = bigHouse.put();
System.out.println("生产汽车-->count = " + count);
}
}
}
/**
* 姑且买车的人当做是消费者线程
*/
public static class Consumer implements Runnable {
private CarBigHouse bigHouse;
public Consumer(CarBigHouse bigHouse) {
this.bigHouse = bigHouse;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {// 当做消费者线程,从仓库里边提取汽车,其实是触发,从仓库里边提取一辆汽车出来
int count = bigHouse.get();
System.out.println("消费汽车-->count = " + count);
}
}
}
/**
* 这边姑且当做是车子big house放车子的仓库房
*/
public static class CarBigHouse {
public int carNums = 0;// 这边是仓库房子中车子的数量总数
public List<Car> carList = new ArrayList<Car>();// 这边模拟用来放汽车的list
public static final int max = 100;// 简单设置下,做下上限设置
private Object lock = new Object();// 采用object的wait和notify方式处理同步问题
public int put() {// 提供给生产者放汽车到仓库的接口
synchronized (lock) {
if (carList.size() == max) {// 达到了上限,不再生产car
try {
lock.wait();// 进行阻塞处理
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Car car = CarFactory.makeNewCar();
carList.add(car);// 加到仓库中去
carNums++;// 总数增加1
lock.notify();// 唤醒等待的线程
return carNums;
}
}
public int get() {// 提供给消费者从这边取汽车接口
Car car = null;
synchronized (lock) {
if (carList.size() == 0) {// 没有汽车可以用来消费
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (carList.size() != 0) {// size不为空才去取车
car = carList.get(carList.size() - 1);// 提取最后一个car
carList.remove(car);// 从从库list中移除掉
carNums--;// 总数减少1
}
lock.notify();
return carNums;
}
}
public static class Car {
public String carName;// 汽车名称
public double carPrice;// 汽车价格
public Car() {
}
public Car(String carName, double carPrice) {
this.carName = carName;
this.carPrice = carPrice;
}
}
}
/**
* 采用静态工厂方式创建car对象,这个只是简单模拟,不做设计模式上的过多考究
*/
public static class CarFactory {
private CarFactory() {
}
public static Car makeNewCar(String carName, double carPrice) {
return new Car(carName, carPrice);
}
public static Car makeNewCar() {
return new Car();
}
}
/**
* 第二个版本的生产者和消费者线程,加上了同步机制的方法
*
* @param args
*/
public static void main(String[] args) {
CarBigHouse bigHouse = new CarBigHouse();
new Thread(new CarSeller(bigHouse)).start();
new Thread(new Consumer(bigHouse)).start();
}
}
线程池、线程同步、互斥锁、读写锁、原子数、唤醒、通知、信号量、线程交换队列
线程池
推荐用ThreadPoolExecutor的工厂构造类Executors来管理线程池,线程复用线程池开销较每次申请新线程小,具体看代码以及注释
publicclassTestThread {
/**
* 使用线程池的方式是复用线程的(推荐)
* 而不使用线程池的方式是每次都要创建线程
* Executors.newCachedThreadPool(),该方法返回的线程池是没有线程上限的,可能会导致过多的内存占用
* 建议使用Executors.newFixedThreadPool(n)
*
* 有兴趣还可以看下定时线程池:SecheduledThreadPoolExecutor
*/
publicstaticvoidmain(String[] args) throwsInterruptedException, ExecutionException {
intnThreads = 5;
/**
* Executors是ThreadPoolExecutor的工厂构造方法
*/
ExecutorService executor = Executors.newFixedThreadPool(nThreads);
//submit有返回值,而execute没有返回值,有返回值方便Exception的处理
Future res = executor.submit(newConsumerThread());
//executor.execute(new ConsumerThread());
/**
* shutdown调用后,不可以再submit新的task,已经submit的将继续执行
* shutdownNow试图停止当前正执行的task,并返回尚未执行的task的list
*/
executor.shutdown();
//配合shutdown使用,shutdown之后等待所有的已提交线程运行完,或者到超时。继续执行后续代码
executor.awaitTermination(1, TimeUnit.DAYS);
//打印执行结果,出错的话会抛出异常,如果是调用execute执行线程那异常会直接抛出,不好控制,submit提交线程,调用res.get()时才会抛出异常,方便控制异常
System.out.println("future result:"+res.get());
}
staticclassConsumerThread implementsRunnable{
@Override
publicvoidrun() {
for(inti=0;i<5;i++) {
System.out.println(i);
}
}
}
}
输出:
0
1
2
3
4
future result:null
线程同步
synchronized(this)和synchronized(MyClass.class)区别:前者与加synchronized的成员方法互斥,后者和加synchronized的静态方法互斥
synchronized的一个应用场景是单例模式的,双重检查锁
publicclassSingleton {
privatevolatilestaticSingleton singleton;
privateSingleton (){}
publicstaticSingleton getSingleton() {
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) {
singleton = newSingleton();
}
}
}
returnsingleton;
}
}
注意:不过双重检查锁返回的实例可能是没有构造完全的对象,高并发的时候直接使用有问题,不知道在新版的java里是否解决了
所以有了内部类方式的单例模式,这样的单例模式有了延迟加载的功能(还有一种枚举方式的单例模式,用的不多,有兴趣的可以上网查)
//(推荐)延迟加载的单例模式
publicclassSingleton {
privatestaticclassSingletonHolder {
privatestaticfinalSingleton INSTANCE = newSingleton();
}
privateSingleton (){}
publicstaticfinalSingleton getInstance() {
returnSingletonHolder.INSTANCE;
}
}
若不要延迟加载,在类加载的时候实例化对象,那直接这么写,如下:
publicclassSingleton {
privatestaticSingleton instance = newSingleton();
privateSingleton (){}
publicstaticSingleton getInstance() {
returninstance;
}
}
volatile保证同一变量在多线程中的可见性,所以它更多是用于修饰作为开关状态的变量
用synchronized修饰变量的get和set方法,不但可以保证和volatile修饰变量一样的效果(获取最新值),因为synchronized不仅会把当前线程修改的变量的本地副本同步给主存,还会从主存中读取数据更新本地副本。而且synchronized还有互斥的效果,可以有效控制并发修改一个值,因为synchronized保证代码块的串行执行。如果只要求获取最新值的特性,用volatile就好,因为volatile比较轻量,性能较好。
互斥锁、读写锁
ReentrantLock 和ReentrantReadWriteLock
JDK5增加了ReentrantLock这个类因为两点:
1.ReentrantLock提供了tryLock方法,tryLock调用的时候,如果锁被其他线程(同一个线程两次调用tryLock也都返回true)持有,那么tryLock会立即返回,返回结果是false。lock()方法会阻塞。
2.构造RenntrantLock对象可以接收一个boolean类型的参数,描述锁公平与否的函数。公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;非公平锁的好处是整体效率相对高一些。
注意:使用ReentrantLock后,需要显式地进行unlock,所以建议在finally块中释放锁,如下:
lock.lock();try{
//do something }finally{
lock.unlock();
}
ReentrantReadWriteLock与ReentrantLock的用法类似,差异是前者通过readLock()和writeLock()两个方法获得相关的读锁和写锁操作。
原子数
除了用互斥锁控制变量的并发修改之外,jdk5中还增加了原子类,通过比较并交换(硬件CAS指令)来避免线程互斥等待的开销,进而完成超轻量级的并发控制,一般用来高效的获取递增计数器。
AtomicInteger counter = newAtomicInteger();
counter.incrementAndGet();
counter.decrementAndGet();
可以简单的理解为以下代码,增加之后与原先值比较,如果发现增长不一致则循环这个过程。代码如下
publicclassCasCounter {
privateSimulatedCAS value;
publicintgetValue() {
returnvalue.getValue();
}
publicintincrement() {
intoldValue = value.getValue();
while(value.compareAndSwap(oldValue, oldValue + 1) != oldValue)
oldValue = value.getValue();
returnoldValue + 1;
}
}
可以看IBM工程师的一篇文章 Java 理论与实践: 流行的原子
唤醒、通知
wait,notify,notifyAll是java的Object对象上的三个方法,多线程中可以用这些方法完成线程间的状态通知。
notify是唤醒一个等待线程,notifyAll会唤醒所有等待线程。
CountDownLatch主要提供的机制是当多个(具体数量等于初始化CountDownLatch时的count参数的值)线程都到达了预期状态或完成预期工作时触发事件,其他线程可以等待这个事件来触发后续工作。
举个例子,大数据分拆给多个线程进行排序,比如主线程
CountDownLatch latch = newCountDownLatch(5);
for(inti=0;i<5;i++) {
threadPool.execute(newMyRunnable(latch,datas));
}
latch.await();
//do something 合并数据
MyRunnable的实现代码如下
publicvoidrun() {
//do something数据排序 latch.countDown();
//继续自己线程的工作,与CyclicBarrier最大的不同,稍后马上讲
}
CyclicBarrier循环屏障,协同多个线程,让多个线程在这个屏障前等待,直到所有线程都到达了这个屏障时,再一起继续执行后面的动作。
使用CyclicBarrier可以重写上面的排序代码
主线程如下
CyclicBarrier barrier = newCyclicBarrier(5+1); //主线程也要消耗一个await,所以+1
for(inti=0;i<5;i++) {
threadPool.execute(newMyRunnable(barrier,datas));//如果线程池线程数过少,就会发生死锁
}
barrier.await();//合并数据
MyRunnable代码如下
publicvoidrun() {
//数据排序barrier.await();
}
//全部 count+1 await之后(包括主线程),之后的代码才会一起执行
信号量
Semaphore用于管理信号量,与锁的最大区别是,可以通过令牌的数量,控制并发数量,当管理的信号量只有1个时,就退化到互斥锁。
例如我们需要控制远程方法的并发量,代码如下
semaphore.acquire(count);try{
//调用远程方法 }finally{
semaphore.release(count);
}
线程交换队列
Exchanger用于在两个线程之间进行数据交换,线程会阻塞在Exchanger的exchange方法上,直到另外一个线程也到了同一个Exchanger的exchanger方法时,二者进行交换,然后两个线程继续执行自身相关代码。
publicclassTestExchanger {
staticExchanger exchanger = newExchanger();
publicstaticvoidmain(String[] args) {
newThread() {
publicvoidrun() {
inta = 1;
try{
a = (int) exchanger.exchange(a);
} catch(Exception e) {
e.printStackTrace();
}
System.out.println("Thread1: "+a);
}
}.start();
newThread() {
publicvoidrun() {
inta = 2;
try{
a = (int) exchanger.exchange(a);
} catch(Exception e) {
e.printStackTrace();
}
System.out.println("Thread2: "+a);
}
}.start();
}
}
输出结果:
Thread2: 1
Thread1: 2
并发容器
CopyOnWrite思路是在更改容器时,把容器写一份进行修改,保证正在读的线程不受影响,适合应用在读多写少的场景,因为写的时候重建一次容器。
以Concurrent开头的容器尽量保证读不加锁,并且修改时不影响读,所以会达到比使用读写锁更高的并发性能
Java NIO:浅析I/O模型
也许很多朋友在学习NIO的时候都会感觉有点吃力,对里面的很多概念都感觉不是那么明朗。在进入Java NIO编程之前,我们今天先来讨论一些比较基础的知识:I/O模型。下面本文先从同步和异步的概念 说起,然后接着阐述了阻塞和非阻塞的区别,接着介绍了阻塞IO和非阻塞IO的区别,然后介绍了同步IO和异步IO的区别,接下来介绍了5种IO模型,最后介绍了两种和高性能IO设计相关的设计模式(Reactor和Proactor)。
以下是本文的目录大纲:
一.什么是同步?什么是异步?
二.什么是阻塞?什么是非阻塞?
三.什么是阻塞IO?什么是非阻塞IO?
四.什么是同步IO?什么是异步IO?
五.五种IO模型
六.两种高性能IO设计模式
若有不正之处,请多多谅解并欢迎批评指正。
请尊重作者劳动成果,转载请标明原文链接:
http://www.cnblogs.com/dolphin0520/p/3916526.html
一.什么是同步?什么是异步?
同步和异步的概念出来已经很久了,网上有关同步和异步的说法也有很多。以下是我个人的理解:
同步就是:如果有多个任务或者事件要发生,这些任务或者事件必须逐个地进行,一个事件或者任务的执行会导致整个流程的暂时等待,这些事件没有办法并发地执行;
异步就是:如果有多个任务或者事件发生,这些事件可以并发地执行,一个事件或者任务的执行不会导致整个流程的暂时等待。
这就是同步和异步。举个简单的例子,假如有一个任务包括两个子任务A和B,对于同步来说,当A在执行的过程中,B只有等待,直至A执行完毕,B才能执行;而对于异步就是A和B可以并发地执行,B不必等待A执行完毕之后再执行,这样就不会由于A的执行导致整个任务的暂时等待。
如果还不理解,可以先看下面这2段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
voidfun1() {
}
voidfun2() {
}
voidfunction(){ fun1(); fun2() ..... ..... } |
这段代码就是典型的同步,在方法function中,fun1在执行的过程中会导致后续的fun2无法执行,fun2必须等待fun1执行完毕才可以执行。
接着看下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
voidfun1() {
}
voidfun2() {
}
voidfunction(){ newThread(){ publicvoidrun() { fun1(); } }.start();
newThread(){ publicvoidrun() { fun2(); } }.start();
..... ..... } |
这段代码是一种典型的异步,fun1的执行不会影响到fun2的执行,并且fun1和fun2的执行不会导致其后续的执行过程处于暂时的等待。
事实上,同步和异步是一个非常广的概念,它们的重点在于多个任务和事件发生时,一个事件的发生或执行是否会导致整个流程的暂时等待。我觉得可以将同步和异步与Java中的synchronized关键字联系起来进行类比。当多个线程同时访问一个变量时,每个线程访问该变量就是一个事件,对于同步来说,就是这些线程必须逐个地来访问该变量,一个线程在访问该变量的过程中,其他线程必须等待;而对于异步来说,就是多个线程不必逐个地访问该变量,可以同时进行访问。
因此,个人觉得同步和异步可以表现在很多方面,但是记住其关键在于多个任务和事件发生时,一个事件的发生或执行是否会导致整个流程的暂时等待。一般来说,可以通过多线程的方式来实现异步,但是千万记住不要将多线程和异步画上等号,异步只是宏观上的一个模式,采用多线程来实现异步只是一种手段,并且通过多进程的方式也可以实现异步。
二.什么是阻塞?什么是非阻塞?
在前面介绍了同步和异步的区别,这一节来看一下阻塞和非阻塞的区别。
阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足;
非阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。
这就是阻塞和非阻塞的区别。也就是说阻塞和非阻塞的区别关键在于当发出请求一个操作时,如果条件不满足,是会一直等待还是返回一个标志信息。
举个简单的例子:
假如我要读取一个文件中的内容,如果此时文件中没有内容可读,对于同步来说就是会一直在那等待,直至文件中有内容可读;而对于非阻塞来说,就会直接返回一个标志信息告知文件中暂时无内容可读。
在网上有一些朋友将同步和异步分别与阻塞和非阻塞画上等号,事实上,它们是两组完全不同的概念。注意,理解这两组概念的区别对于后面IO模型的理解非常重要。
同步和异步着重点在于多个任务的执行过程中,一个任务的执行是否会导致整个流程的暂时等待;
而阻塞和非阻塞着重点在于发出一个请求操作时,如果进行操作的条件不满足是否会返会一个标志信息告知条件不满足。
理解阻塞和非阻塞可以同线程阻塞类比地理解,当一个线程进行一个请求操作时,如果条件不满足,则会被阻塞,即在那等待条件满足。
三.什么是阻塞IO?什么是非阻塞IO?
在了解阻塞IO和非阻塞IO之前,先看下一个具体的IO操作过程是怎么进行的。
通常来说,IO操作包括:对硬盘的读写、对socket的读写以及外设的读写。
当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作,也就是说一个完整的IO读请求操作包括两个阶段:
1)查看数据是否就绪;
2)进行数据拷贝(内核将数据拷贝到用户线程)。
那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。
Java中传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回;而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。
四.什么是同步IO?什么是异步IO?
我们先来看一下同步IO和异步IO的定义,在《Unix网络编程》一书中对同步IO和异步IO的定义是这样的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
An asynchronous I/O operation does not cause the requesting process to be blocked.
从字面的意思可以看出:同步IO即 如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞;
而异步IO为 如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞。
事实上,同步IO和异步IO模型是针对用户线程和内核的交互来说的:
对于同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;
而异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞。
这是同步IO和异步IO关键区别所在,同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持。
注意同步IO和异步IO与阻塞IO和非阻塞IO是不同的两组概念。
阻塞IO和非阻塞IO是反映在当用户请求IO操作时,如果数据没有就绪,是用户线程一直等待数据就绪,还是会收到一个标志信息这一点上面的。也就是说,阻塞IO和非阻塞IO是反映在IO操作的第一个阶段,在查看数据是否就绪时是如何处理的。
五.五种IO模型
在《Unix网络编程》一书中提到了五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
下面就分别来介绍一下这5种IO模型的异同。
1.阻塞IO模型
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
典型的阻塞IO模型的例子为:
1 |
data = socket.read(); |
如果数据没有就绪,就会一直阻塞在read方法。
2.非阻塞IO模型
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
典型的非阻塞IO模型一般如下:
1 2 3 4 5 6 7 |
while(true){ data = socket.read(); if(data!= error){ 处理数据 break; } } |
但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。
3.多路复用IO模型
多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。
也许有朋友会说,我可以采用 多线程+ 阻塞IO 达到类似的效果,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。
而多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。
另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
4.信号驱动IO模型
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
5.异步IO模型
异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作。
注意,异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO。
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
六.两种高性能IO设计模式
在传统的网络服务设计模式中,有两种比较经典的模式:
一种是 多线程,一种是线程池。
对于多线程模式,也就说来了client,服务器就会新建一个线程来处理该client的读写事件,如下图所示:
这种模式虽然处理起来简单方便,但是由于服务器为每个client的连接都采用一个线程去处理,使得资源占用非常大。因此,当连接数量达到上限时,再有用户请求连接,直接会导致资源瓶颈,严重的可能会直接导致服务器崩溃。
因此,为了解决这种一个线程对应一个客户端模式带来的问题,提出了采用线程池的方式,也就说创建一个固定大小的线程池,来一个客户端,就从线程池取一个空闲线程来处理,当客户端处理完读写操作之后,就交出对线程的占用。因此这样就避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。
但是线程池也有它的弊端,如果连接大多是长连接,因此可能会导致在一段时间内,线程池中的线程都被占用,那么当再有用户请求连接时,由于没有可用的空闲线程来处理,就会导致客户端连接失败,从而影响用户体验。因此,线程池比较适合大量的短连接应用。
因此便出现了下面的两种高性能IO设计模式:Reactor和Proactor。
在Reactor模式中,会先对每个client注册感兴趣的事件,然后有一个线程专门去轮询每个client是否有事件发生,当有事件发生时,便顺序处理每个事件,当所有事件处理完之后,便再转去继续轮询,如下图所示:
从这里可以看出,上面的五种IO模型中的多路复用IO就是采用Reactor模式。注意,上面的图中展示的 是顺序处理每个事件,当然为了提高事件处理速度,可以通过多线程或者线程池的方式来处理事件。
在Proactor模式中,当检测到有事件发生时,会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已完成,可以得知,异步IO模型采用的就是Proactor模式。
今天刚刚看完java的io流操作,把主要的脉络看了一遍,不能保证以后使用时都能得心应手,但是最起码用到时知道有这么一个功能可以实现,下面对学习进行一下简单的总结:
IO流主要用于硬板、内存、键盘等处理设备上得数据操作,根据处理数据的数据类型的不同可以分为:字节流(抽象基类为InPutStream和OutPutStream)和字符流(抽象基类为Reader和Writer)。根据流向不同,可以分为:输入流和输出流。 其中主要结构可以用下图来表示:
字符流和字节流的主要区别:
1.字节流读取的时候,读到一个字节就返回一个字节; 字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在UTF-8码表中是3个字节)时。先去查指定的编码表,将查到的字符返回。
2.字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。
IO流主要可以分为节点流和处理流两大类。
一、节点流类型
该类型可以从或者向一个特定的地点或者节点读写数据。主要类型如下:
类型 |
字符流 |
字节流 |
File(文件) |
FileReader FileWriter |
FileInputStream FileOutputSream |
Memory Array |
CharArrayReader CharArrayWriter |
ByteArrayInputStream ByteArrayOutputSream |
Memory String |
StringReader StringWriter |
- |
Pipe(管道) |
PipedReader PipedWriter |
PipedInputSream PipedOutputSream |
二、处理流类型
该类型是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写,处理流的构造方法总是要带一个其他流对象作为参数,一个流对象进过其他流的多次包装,叫做流的链接。主要可以分为以下几种:
1、缓冲流(BufferedInPutStream/BufferedOutPutStream和BufferedWriter/BufferedReader)他可以提高对流的操作效率。
写入缓冲区对象:
[java]view plaincopy
1.BufferedWriterbufw=newBufferedWriter(newFileWriter("buf.txt"));
读取缓冲区对象:
[java]view plaincopy
1.BufferedReaderbufr=newBufferedReader(newFileReader("buf.txt"));
该类型的流有一个特有的方法:readLine();一次读一行,到行标记时,将行标记之前的字符数据作为字符串返回,当读到末尾时,返回null,其原理还是与缓冲区关联的流对象的read方法,只不过每一次读取到一个字符,先不进行具体操作,先进行临时储存,当读取到回车标记时,将临时容器中储存的数据一次性返回。
2、转换流(InputStreamReader/OutputStreamWriter)
该类型时字节流和字符流之间的桥梁,该流对象中可以对读取到的字节数据进行指定编码的编码转换。
构造函数主要有:
[java]view plaincopy
1.InputStreamReader(InputStream);//通过构造函数初始化,使用的是本系统默认的编码表GBK。
2.InputStreamWriter(InputStream,StringcharSet);//通过该构造函数初始化,可以指定编码表。
3.OutputStreamWriter(OutputStream);//通过该构造函数初始化,使用的是本系统默认的编码表GBK。
4.OutputStreamwriter(OutputStream,StringcharSet);//通过该构造函数初始化,可以指定编码表。
注意:在使用FileReader操作文本数据时,该对象使用的时默认的编码表,即
FileReader fr=new FileReader(“a.txt”); 与 InputStreamReader isr=new InputStreamReader(new FileInputStream("a.txt")); 的意义相同。如果要使用指定表编码表时,必须使用转换流,即如果a.txt中的文件中的字符数据是通过utf-8的形式编码,那么在读取时,就必须指定编码表,那么转换流时必须的。即:
InputStreamReader isr=new InputStreamReader(new FileInputStream("a.txt"),utf-8);
3、数据流(DataInputStream/DataOutputStream)
该数据流可以方便地对一些基本类型数据进行直接的存储和读取,不需要再进一步进行转换,通常只要操作基本数据类型的数据,就需要通过DataStream进行包装。
构造方法:
[java]view plaincopy
1.DataInputStreamReader(InputStream);
2.DataInputStreamWriter(OutputStream);
方法举例:
[java]view plaincopy
1.intreadInt();//一次读取四个字节,并将其转成int值
2.writeInt(int);//一次写入四个字节,注意和write(int)不同,write(int)只将该整数的最低一个8位写入,剩余三个8为丢失
3.hortreadShort();
4.writeShort(short);
5.StringreadUTF();//按照utf-8修改版读取字符,注意,它只能读writeUTF()写入的字符数据。
6.writeUTF(String);//按照utf-8修改版将字符数据进行存储,只能通过readUTF读取。
注意:在使用数据流读/存数据的时候,需要有一定的顺序,即某个类型的数据先写入就必须先读出,服从先进先出的原则。
四、打印流(PrintStream/PrintWriter)
PrintStream是一个字节打印流,System.out对应的类型就是PrintStream,它的构造函数可以接受三种数据类型的值:1.字符串路径。2.File对象 3.OutputStream
PrintStream是一个字符打印流,它的构造函数可以接受四种类型的值:1.字符串路径。2.File对象 3.OutputStream 4.Writer 对于1、2类型的数据,可以指定编码表,也就是字符集,对于3、4类型的数据,可以指定自动刷新,当该自动刷新为True时,只有3个方法可以用:println,printf,format。
五、对象流(ObjectInputStream/ObjectOutputStream)
该类型的流可以把类作为一个整体进行存取,主要方法有:
Object readObject();该方法抛出异常:ClassNotFountException。
void writeObject(Object):被写入的对象必须实现一个接口:Serializable,否则就会抛出:NotSerializableException
java Socket用法详解
在客户/服务器通信模式中, 客户端需要主动创建与服务器连接的 Socket(套接字), 服务器端收到了客户端的连接请求, 也会创建与客户连接的 Socket. Socket可看做是通信连接两端的收发器, 服务器与客户端都通过 Socket 来收发数据.
这篇文章首先介绍Socket类的各个构造方法, 以及成员方法的用法, 接着介绍 Socket的一些选项的作用, 这些选项可控制客户建立与服务器的连接, 以及接收和发送数据的行为.
一. 构造Socket
Socket的构造方法有以下几种重载形式:
Socket()
Socket(InetAddress address, int port) throws UnknowHostException, IOException
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
Socket(String host, int port) throws UnknowHostException, IOException
Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
除了第一个不带参数的构造方法以外, 其他构造方法都会试图建立与服务器的连接, 如果连接成功, 就返回 Socket对象; 如果因为某些原因连接失败, 就会抛出IOException .
1.1 使用无参数构造方法, 设定等待建立连接的超时时间
Socket socket = new Socket();
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
socket.connect(remoteAddr, 60000); //等待建立连接的超时时间为1分钟
以上代码用于连接到本地机器上的监听8000端口的服务器程序, 等待连接的最长时间为1分钟. 如果在1分钟内连接成功则connet()方法顺利返回; 如果在1分钟内出现某种异常, 则抛出该异常; 如果超过1分钟后, 即没有连接成功, 也没有出现其他异常, 那么会抛出 SocketTimeoutException. Socket 类的 connect(SocketAddress endpoint, int timeout) 方法负责连接服务器, 参数endpoint 指定服务器的地址, 参数timeout 设定超时数据, 以毫秒为单位. 如果参数timeout 设为0, 表示永远不会超时, 默认是不会超时的.
1.2 设定服务器的地址
除了第一个不带参数的构造方法, 其他构造方法都需要在参数中设定服务器的地址, 包括服务器的IP地址或主机名, 以及端口:
Socket(InetAddress address, int port) //第一个参数address 表示主机的IP地址
Socket(String host, int port) //第一个参数host 表示主机的名字
InetAddress 类表示服务器的IP地址, InetAddress 类提供了一系列静态工厂方法, 用于构造自身的实例, 例如:
//返回本地主机的IP地址
InetAddress addr1 = InetAddress.getLocalHost();
//返回代表 "222.34.5.7"的 IP地址
InetAddress addr2 = InetAddress.getByName("222.34.5.7");
//返回域名为"www.javathinker.org"的 IP地址
InetAddress addr3 = InetAddress.getByName("www.javathinker.org");
1.3 设定客户端的地址
在一个Socket 对象中, 即包含远程服务器的IP 地址和端口信息, 也包含本地客户端的IP 地址和端口信息. 默认情况下, 客户端的IP 地址来自于客户程序所在的主机, 客户端的端口则由操作系统随机分配. Socket类还有两个构造方法允许显式地设置客户端的IP 地址和端口:
//参数localAddr 和 localPort 用来设置客户端的IP 地址和端口
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
如果一个主机同时属于两个以上的网络, 它就可能拥有两个以上的IP 地址. 例如, 一个主机在Internet 网络中的IP 地址为 "222.67.1.34", 在一个局域网中的IP 地址为 "112.5.4.3". 假定这个主机上的客户程序希望和同一个局域网的一个服务器程序(地址为:"112.5.4.45: 8000")通信, 客户端可按照如下方式构造Socket 对象:
InetAddress remoteAddr1 = InetAddress.getByName("112.5.4.45");
InetAddress localAddr1 = InetAddress.getByName("112.5.4.3");
Socket socket1 = new Socket(remoteAddr1, 8000, localAddr1, 2345); //客户端使用端口2345
1.4 客户连接服务器时可能抛出的异常
当Socket 的构造方法请求连接服务器时, 可能会抛出下面的异常.
UnKnownHostException: 如果无法识别主机的名字或IP 地址, 就会抛出这种异常.
ConnectException: 如果没有服务器进程监听指定的端口, 或者服务器进程拒绝连接, 就会抛出这种异常.
SocketTimeoutException: 如果等待连接超时, 就会抛出这种异常.
BindException: 如果无法把Socket 对象与指定的本地IP 地址或端口绑定, 就会抛出这种异常.
以上4中异常都是IOException的直接或间接子类. 如图2-1所示.
IOException------- UnknownHostException
|---- InterruptedIOException ----------- SocketTimeoutException
|---- SocketException ----------- BindException
|---------- ConnectException
图2-1 客户端连接服务器时可能抛出的异常
二. 获取Socket 的信息
在一个Socket 对象中同时包含了远程服务器的IP 地址和端口信息, 以及客户本地的IP 地址和端口信息. 此外, 从Socket 对象中还可以获得输出流和输入流, 分别用于向服务器发送数据, 以及接收从服务器端发来的数据. 以下方法用于获取Socket的有关信息.
getInetAddress(): 获得远程服务器的IP 地址.
getPort(): 获得远程服务器的端口.
getLocalAddress(): 获得客户本地的IP 地址.
getLocalPort(): 获得客户本地的端口.
getInputStream(): 获得输入流. 如果Socket 还没有连接, 或者已经关闭, 或者已经通过 shutdownInput() 方法关闭输入流, 那么此方法会抛出IOException.
getOutputStream(): 获得输出流, 如果Socket 还没有连接, 或者已经关闭, 或者已经通过 shutdownOutput() 方法关闭输出流, 那么此方法会抛出IOException.
这里有个HTTPClient 类的例子, 代码我是写好了, 也测试过了, 因为篇幅原因就不贴了. 这个HTTPClient 类用于访问网页www.javathinker.org/index.jsp. 该网页位于一个主机名(也叫域名)为www.javathinker.org的远程HTTP服务器上, 它监听 80 端口. 在HTTPClient 类中, 先创建了一个连接到该HTTP服务器的Socket对象, 然后发送符合HTTP 协议的请求, 接着接收从HTTP 服务器上发回的响应结果.
三. 关闭Socket
当客户与服务器的通信结束, 应该及时关闭Socket , 以释放Socket 占用的包括端口在内的各种资源. Socket 的 close() 方法负责关闭Socket. 当一个Socket对象被关闭, 就不能再通过它的输入流和输出流进行I/O操作, 否则会导致IOException.
为了确保关闭Socket 的操作总是被执行, 强烈建议把这个操作放在finally 代码块中:
Socket socket = null;
try{
socket = new Socket(www.javathinker.org,80);
//执行接收和发送数据的操作
..........
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(socket != null) socket.close();
}catch(IOException e){e.printStackTrace();}
}
Socket 类提供了3 个状态测试方法.
isClosed(): 如果Socket已经连接到远程主机, 并且还没有关闭, 则返回true , 否则返回false .
isConnected(): 如果Socket曾经连接到远程主机, 则返回true , 否则返回false .
isBound(): 如果Socket已经与一个本地端口绑定, 则返回true , 否则返回false .
如果要判断一个Socket 对象当前是否处于连接状态, 可采用以下方式:
boolean isConnected = socket.isConnected() && !socket.isClosed();
四. 半关闭Socket
进程A 与进程B 通过Socket 通信, 假定进程A 输出数据, 进程B 读入数据. 进程A 如何告诉进程B 所有数据已经输出完毕? 下文略......
五. 设置Socket 的选项
Socket 有以下几个选项.
TCP_NODELAY: 表示立即发送数据.
SO_RESUSEADDR: 表示是否允许重用Socket 所绑定的本地地址.
SO_TIMEOUT: 表示接收数据时的等待超时数据.
SO_LINGER: 表示当执行Socket 的 close()方法时, 是否立即关闭底层的Socket.
SO_SNFBUF: 表示发送数据的缓冲区的大小.
SO_RCVBUF: 表示接收数据的缓冲区的大小.
SO_KEEPALIVE: 表示对于长时间处于空闲状态的Socket , 是否要自动把它关闭.
OOBINLINE: 表示是否支持发送一个字节的TCP 紧急数据.
5.1 TCP_NODELAY 选项
设置该选项: public void setTcpNoDelay(boolean on) throws SocketException
读取该选项: public boolean getTcpNoDelay() throws SocketException
默认情况下, 发送数据采用Negale 算法. Negale 算法是指发送方发送的数据不会立即发出, 而是先放在缓冲区, 等缓存区满了再发出. 发送完一批数据后, 会等待接收方对这批数据的回应, 然后再发送下一批数据. Negale 算法适用于发送方需要发送大批量数据, 并且接收方会及时作出回应的场合, 这种算法通过减少传输数据的次数来提高通信效率.
如果发送方持续地发送小批量的数据, 并且接收方不一定会立即发送响应数据, 那么Negale 算法会使发送方运行很慢. 对于GUI 程序, 如网络游戏程序(服务器需要实时跟踪客户端鼠标的移动), 这个问题尤其突出. 客户端鼠标位置改动的信息需要实时发送到服务器上, 由于Negale 算法采用缓冲, 大大减低了实时响应速度, 导致客户程序运行很慢.
TCP_NODELAY 的默认值为 false, 表示采用 Negale 算法. 如果调用setTcpNoDelay(true)方法, 就会关闭 Socket的缓冲, 确保数据及时发送:
if(!socket.getTcpNoDelay()) socket.setTcpNoDelay(true);
如果Socket 的底层实现不支持TCP_NODELAY 选项, 那么getTcpNoDelay() 和 setTcpNoDelay 方法会抛出 SocketException.
5.2 SO_RESUSEADDR 选项
设置该选项: public void setResuseAddress(boolean on) throws SocketException
读取该选项: public boolean getResuseAddress() throws SocketException
当接收方通过Socket 的close() 方法关闭Socket 时, 如果网络上还有发送到这个Socket 的数据, 那么底层的Socket 不会立即释放本地端口, 而是会等待一段时间, 确保接收到了网络上发送过来的延迟数据, 然后再释放端口. Socket接收到延迟数据后, 不会对这些数据作任何处理. Socket 接收延迟数据的目的是, 确保这些数据不会被其他碰巧绑定到同样端口的新进程接收到.
客户程序一般采用随机端口, 因此出现两个客户程序绑定到同样端口的可能性不大. 许多服务器程序都使用固定的端口. 当服务器程序关闭后, 有可能它的端口还会被占用一段时间, 如果此时立刻在同一个主机上重启服务器程序, 由于端口已经被占用, 使得服务器程序无法绑定到该端口, 启动失败. (第三篇文章会对此作出介绍).
为了确保一个进程关闭Socket 后, 即使它还没释放端口, 同一个主机上的其他进程还可以立即重用该端口, 可以调用Socket 的setResuseAddress(true) 方法:
if(!socket.getResuseAddress()) socket.setResuseAddress(true);
值得注意的是 socket.setResuseAddress(true) 方法必须在 Socket 还没有绑定到一个本地端口之前调用, 否则执行 socket.setResuseAddress(true) 方法无效. 因此必须按照以下方式创建Socket 对象, 然后再连接远程服务器:
Socket socket = new Socket(); //此时Socket对象未绑定本地端口,并且未连接远程服务器
socket.setReuseAddress(true);
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
socket.connect(remoteAddr); //连接远程服务器, 并且绑定匿名的本地端口
或者:
Socket socket = new Socket(); //此时Socke 对象为绑定本地端口, 并且未连接远程服务器
socket.setReuseAddress(true);
SocketAddress localAddr = new InetSocketAddress("localhost",9000);
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
socket.bind(localAddr); //与本地端口绑定
socket.connect(remoteAddr); //连接远程服务器
此外, 两个共用同一个端口的进程必须都调用 socket.setResuseAddress(true) 方法, 才能使得一个进程关闭 Socket后, 另一个进程的 Socket 能够立即重用相同端口.
5.3 SO_TIMEOUT 选项
设置该选项: public void setSoTimeout(int milliseconds) throws SocketException
读取该选项: public int getSoTimeout() throws SocketException
当通过Socket 的输入流读数据时, 如果还没有数据, 就会等待. 例如, 在以下代码中, in.read(buff) 方法从输入流中读入 1024个字节:
byte[] buff = new byte[1024];
InputStream in = socket.getInputStream();
in.read(buff);
如果输入流中没有数据, in.read(buff) 就会等待发送方发送数据, 直到满足以下情况才结束等待:
略...............
Socket 类的 SO_TIMEOUT 选项用于设定接收数据的等待超时时间, 单位为毫秒, 它的默认值为 0, 表示会无限等待, 永远不会超时. 以下代码把接收数据的等待超时时间设为 3 分钟:
if(socket.getSoTimeout() == 0) socket.setSoTimeout(60000 * 3); //注意, 原书中这里的代码错误, 里面的方法名字都少了"So"
Socket 的 setSoTimeout() 方法必须在接收数据之前执行才有效. 此外, 当输入流的 read()方法抛出 SocketTimeoutException 后, Socket 仍然是连接的, 可以尝试再次读数据:
socket.setSoTimeout(180000);
byte[] buff = new byte[1024];
InputStream in = socket.getInputStream();
int len = -1;
do{
try{
len = in.read(buff);
//处理读到的数据
//.........
}catch(SocketTimeoutException e){
//e.printStackTrace();
System.out.println("等待读超时!");
len = 0;
}
}while(len != -1);
例子ReceiveServer.java 和 SendClient.java 是一对简单的服务器/客户程序. sendClient 发送字符串 "hello everyone" ,接着睡眠 1 分钟, 然后关闭 Socket. ReceiveServer 读取 SendClient 发送来的数据, 直到抵达输入流的末尾, 最后打印 SendClient 发送来的数据.
ReceiveServer.java 略....... , SendClient.java 略..........
在 SendClient 发送字符串 "hello everyone" 后, 睡眠 1 分钟. 当 SendClient 在睡眠时, ReceiveServer 在执行 in.read(buff) 方法, 不能读到足够的数据填满 buff 缓冲区, 因此会一直等待 SendClient 发送数据. 如果在 ReceiveServer 类中 socket.setSoTimeout(20000) , 从而把等待接收数据的超时时间设为 20 秒, 那么 ReceiveServer 在等待数据时, 每当超过 20 秒, 就会抛出SocketTimeoutException . 等到 SendClient 睡眠 1 分钟后, SendClient 调用 Socket 的 close() 方法关闭 Socket, 这意味着 ReceiveServer 读到了输入流的末尾, ReceiveServer 立即结束读等待, read() 方法返回 -1 . ReceiveServer最后打印接收到的字符串 "hello everyone", 结果如下:
等待读超时!
等待读超时!
hello everyone
5.4 SO_LINGER 选项
设置该选项: public void setSoLinger(boolean on, int seconds) throws SocketException
读取该选项: public int getSoLinger() throws SocketException
SO_LINGER 选项用来控制 Socket 关闭时的行为. 默认情况下, 执行 Socket 的 close() 方法, 该方法会立即返回, 但底层的 Socket 实际上并不立即关闭, 它会延迟一段时间, 直到发送完所有剩余的数据, 才会真正关闭 Socket, 断开连接.
如果执行以下方法:
socket.setSoLinger(true, 0);
那么执行Socket 的close() 方法, 该方法也会立即返回, 并且底层的 Socket 也会立即关闭, 所有未发送完的剩余数据被丢弃.
如果执行以下方法:
socket.setSoLinger(true, 3600);
那么执行Socket 的 close() 方法, 该方法不会立即返回, 而是进入阻塞状态. 同时, 底层的 Socket 会尝试发送剩余的数据. 只有满足以下两个条件之一, close() 方法才返回:
⑴ 底层的 Socket 已经发送完所有的剩余数据;
⑵ 尽管底层的 Socket 还没有发送完所有的剩余数据, 但已经阻塞了 3600 秒(注意这里是秒, 而非毫秒), close() 方法的阻塞时间超过 3600 秒, 也会返回, 剩余未发送的数据被丢弃.
值得注意的是, 在以上两种情况内, 当close() 方法返回后, 底层的 Socket 会被关闭, 断开连接. 此外, setSoLinger(boolean on, int seconds) 方法中的 seconds 参数以秒为单位, 而不是以毫秒为单位.
如果未设置 SO_LINGER 选项, getSoLinger() 返回的结果是 -1, 如果设置了 socket.setSoLinger(true, 80) , getSoLinger() 返回的结果是 80.
Tips: 当程序通过输出流写数据时, 仅仅表示程序向网络提交了一批数据, 由网络负责输送到接收方. 当程序关闭 Socket, 有可能这批数据还在网络上传输, 还未到达接收方. 这里所说的 "未发送完的数据" 就是指这种还在网络上传输, 未被接收方接收的数据.
例子 SimpleClient.java 与 SimpleServer.java 所示是一对简单的客户/服务器程序. SimpleClient 类发送一万个字符给 SimpleServer, 然后调用Socket 的 close() 方法关闭 Socket.
SimpleServer 通过 ServerSocket 的 accept() 方法接受了 SimpleClient 的连接请求后, 并不立即接收客户发送的数据, 而是睡眠 5 秒钟后再接收数据. 等到 SimpleServer 开始接收数据时, SimpleClient 有可能已经执行了 Socket 的close() 方法, 那么 SimpleServer 还能接收到 SimpleClient 发送的数据吗?
SimpleClient.java 略..., SimpleServer.java 略......
SimpleClient.java中
System.out.println("开始关闭 Socket");
long begin = System.currentTimeMillis();
socket.close();
long end = System.currentTimeMillis();
System.out.println("关闭Socket 所用的时间为:" + (end - begin) + "ms");
下面分 3 种情况演示 SimpleClient 关闭 Socket 的行为.
⑴ 未设置 SO_LINGER 选项, 当 SimpleClient 执行 Socket 的close() 方法时, 立即返回, SimpleClient 的打印结果如下:
开始关闭 Socket
关闭Socket 所用的时间为:0ms
等到 SimpleClient 结束运行, SimpleServer 可能才刚刚结束睡眠, 开始接收 SimpleClient 发送的数据. 此时尽管 SimpleClient 已经执行了 Socket 的 close() 方法, 并且 SimpleClient 程序本身也运行结束了, 但从 SimpleServer 的打印结果可以看出, SimpleServer 仍然接收到了所有的数据. 之所以出现这种情况, 是因为当 SimpleClient 执行了 Socket 的 close() 方法后, 底层的 Socket 实际上并没有真正关闭, 与 SimpleServer 的连接依然存在. 底层的 Socket 会存在一段时间, 直到发送完所有的数据.
⑵ 设置SO_LINGER 选项, socket.setSoLinger(true, 0). 这次当 SimpleClient 执行 Socket 的 close() 方法时, 会强行关闭底层的 Socket, 所有未发送完的数据丢失. SimpleClient 的打印结果如下:
开始关闭 Socket
关闭Socket 所用的时间为:0ms
从打印结果看出, SimpleClient 执行 Socket 的 close() 方法时, 也立即返回. 当 SimpleServer 结束睡眠, 开始接收 SimpleClient 发送的数据时, 由于 SimpleClient 已经关闭底层 Socket, 断开连接, 因此 SimpleServer 在读数据时会抛出 SocketException:
java.net.SocketException: Connection reset
⑶ 设置SO_LINGER 选项, socket.setSoLinger(true, 3600). 这次当 SimpleClient 执行 Socket 的close() 方法时, 会进入阻塞状态, 知道等待了 3600 秒, 或者底层 Socket 已经把所有未发送的剩余数据发送完毕, 才会从 close() 方法返回. SimpleClient 的打印结果如下:
开始关闭 Socket
关闭Socket 所用的时间为:5648ms
当 SimpleServer 结束了 5 秒钟的睡眠, 开始接收 SimpleClient 发送的数据时, SimpleClient 还在这些 Socket 的close() 方法, 并且处于阻塞状态. SimpleClient 与 SimpleServer 之间的连接依然存在, 因此 SimpleServer 能够接收到 SimpleClient 发送的所有数据.
5.5 SO_RCVBUF 选项
设置该选项: public void setReceiveBufferSize(int size) throws SocketException
读取该选项: public int getReceiveBufferSize() throws SocketException
SO_RCVBUF 表示 Socket 的用于输入数据的缓冲区的大小. 一般说来, 传输大的连续的数据块(基于HTTP 或 FTP 协议的通信) 可以使用较大的缓冲区, 这可以减少传输数据的次数, 提高传输数据的效率. 而对于交互频繁且单次传送数据量比较小的通信方式(Telnet 和 网络游戏), 则应该采用小的缓冲区, 确保小批量的数据能及时发送给对方. 这种设定缓冲区大小的原则也同样适用于 Socket 的 SO_SNDBUF 选项.
如果底层 Socket 不支持 SO_RCVBUF 选项, 那么 setReceiveBufferSize() 方法会抛出 SocketException.
5.6 SO_SNDBUF 选项
设置该选项: public void setSendBufferSize(int size) throws SocketException
读取该选项: public int getSendBufferSize() throws SocketException
SO_SNDBUF 表示 Socket 的用于输出数据的缓冲区的大小. 如果底层 Socket 不支持 SO_SNDBUF 选项, setSendBufferSize() 方法会抛出 SocketException.
5.7 SO_KEEPALIVE 选项
设置该选项: public void setKeepAlive(boolean on) throws SocketException
读取该选项: public boolean getKeepAlive() throws SocketException //原书中这个方法返回的类型是int
当 SO_KEEPALIVE 选项为 true 时, 表示底层的TCP 实现会监视该连接是否有效. 当连接处于空闲状态(连接的两端没有互相传送数据) 超过了 2 小时时, 本地的TCP 实现会发送一个数据包给远程的 Socket. 如果远程Socket 没有发回响应, TCP实现就会持续尝试 11 分钟, 直到接收到响应为止. 如果在 12 分钟内未收到响应, TCP 实现就会自动关闭本地Socket, 断开连接. 在不同的网络平台上, TCP实现尝试与远程Socket 对话的时限有所差别.
SO_KEEPALIVE 选项的默认值为 false, 表示TCP 不会监视连接是否有效, 不活动的客户端可能会永远存在下去, 而不会注意到服务器已经崩溃.
以下代码把 SO_KEEPALIVE 选项设为 true:
if(!socket.getKeepAlive()) socket.setKeepAlive(true);
5.8 OOBINLINE 选项
设置该选项: public void setOOBInline(boolean on) throws SocketException
读取该选项: public boolean getOOBInline() throws SocketException //原书中这个方法返回的类型是int
当 OOBINLINE 为 true 时, 表示支持发送一个字节的 TCP 紧急数据. Socket 类的 sendUrgentData(int data) 方法用于发送一个字节的 TCP紧急数据.
OOBINLINE 的默认值为 false, 在这种情况下, 当接收方收到紧急数据时不作任何处理, 直接将其丢弃. 如果用户希望发送紧急数据, 应该把 OOBINLINE 设为 true:
socket.setOOBInline(true);
对于即时类应用或者即时类的游戏,HTTP协议很多时候无法满足于我们的需求。这会,Socket对于我们来说就非常实用了。下面是本次学习的笔记。主要分异常类型、交互原理、Socket、ServerSocket、多线程这几个方面阐述。
异常类型
在了解Socket的内容之前,先要了解一下涉及到的一些异常类型。以下四种类型都是继承于IOException,所以很多之后直接弹出IOException即可。
UnkownHostException: 主机名字或IP错误
ConnectException: 服务器拒绝连接、服务器没有启动、(超出队列数,拒绝连接)
SocketTimeoutException: 连接超时
BindException: Socket对象无法与制定的本地IP地址或端口绑定
交互过程
Socket与ServerSocket的交互,下面的图片我觉得已经说的很详细很清楚了。
Socket
构造函数
Socket()
Socket(InetAddress address, int port)throws UnknownHostException, IOException
Socket(InetAddress address, int port, InetAddress localAddress, int localPort)throws IOException
Socket(String host, int port)throwsUnknownHostException,IOException
Socket(String host, int port, InetAddress localAddress, int localPort)throws IOException
除去第一种不带参数的之外,其它构造函数会尝试建立与服务器的连接。如果失败会抛出IOException错误。如果成功,则返回Socket对象。
InetAddress是一个用于记录主机的类,其静态getHostByName(String msg)可以返回一个实例,其静态方法getLocalHost()也可以获得当前主机的IP地址,并返回一个实例。Socket(String host, int port, InetAddress localAddress, int localPort)构造函数的参数分别为目标IP、目标端口、绑定本地IP、绑定本地端口。
Socket方法
getInetAddress(); 远程服务端的IP地址
getPort(); 远程服务端的端口
getLocalAddress() 本地客户端的IP地址
getLocalPort() 本地客户端的端口
getInputStream(); 获得输入流
getOutStream(); 获得输出流
值得注意的是,在这些方法里面,最重要的就是getInputStream()和getOutputStream()了。
Socket状态
isClosed(); //连接是否已关闭,若关闭,返回true;否则返回false
isConnect();//如果曾经连接过,返回true;否则返回false
isBound(); //如果Socket已经与本地一个端口绑定,返回true;否则返回false
如果要确认Socket的状态是否处于连接中,下面语句是很好的判断方式。
booleanisConnection=socket.isConnected() && !socket.isClosed(); //判断当前是否处于连接
半关闭Socket
很多时候,我们并不知道在获得的输入流里面到底读多长才结束。下面是一些比较普遍的方法:
·自定义标识符(譬如下面的例子,当受到“bye”字符串的时候,关闭Socket)
·告知读取长度(有些自定义协议的,固定前几个字节表示读取的长度的)
·读完所有数据
·当Socket调用close的时候关闭的时候,关闭其输入输出流
ServerSocket
构造函数
ServerSocket()throws IOException
ServerSocket(int port)throws IOException
ServerSocket(int port, int backlog)throws IOException
ServerSocket(int port, int backlog, InetAddress bindAddr)throws IOException
注意点:
1. port服务端要监听的端口;backlog客户端连接请求的队列长度;bindAddr服务端绑定IP
2. 如果端口被占用或者没有权限使用某些端口会抛出BindException错误。譬如1~1023的端口需要管理员才拥有权限绑定。
3. 如果设置端口为0,则系统会自动为其分配一个端口;
4.bindAddr用于绑定服务器IP,为什么会有这样的设置呢,譬如有些机器有多个网卡。
5. ServerSocket一旦绑定了监听端口,就无法更改。ServerSocket()可以实现在绑定端口前设置其他的参数。
单线程的ServerSocket例子
publicvoidservice(){
while(true){
Socket socket=null;
try{
socket=serverSocket.accept();//从连接队列中取出一个连接,如果没有则等待
System.out.println("新增连接:"+socket.getInetAddress()+":"+socket.getPort());
...//接收和发送数据
}catch(IOException e){e.printStackTrace();}finally{
try{
if(socket!=null) socket.close();//与一个客户端通信结束后,要关闭Socket
}catch(IOException e){e.printStackTrace();}
}
}
}
多线程的ServerSocket
多线程的好处不用多说,而且大多数的场景都是多线程的,无论是我们的即时类游戏还是IM,多线程的需求都是必须的。下面说说实现方式:
·主线程会循环执行ServerSocket.accept();
·当拿到客户端连接请求的时候,就会将Socket对象传递给多线程,让多线程去执行具体的操作;
实现多线程的方法要么继承Thread类,要么实现Runnable接口。当然也可以使用线程池,但实现的本质都是差不多的。
这里举例:
下面代码为服务器的主线程。为每个客户分配一个工作线程:
publicvoidservice(){
while(true){
Socket socket=null;
try{
socket=serverSocket.accept(); //主线程获取客户端连接
Thread workThread=newThread(newHandler(socket)); //创建线程
workThread.start(); //启动线程
}catch(Exception e){
e.printStackTrace();
}
}
}
当然这里的重点在于如何实现Handler这个类。Handler需要实现Runnable接口:
classHandler implementsRunnable{
privateSocket socket;
publicHandler(Socket socket){
this.socket=socket;
}
publicvoidrun(){
try{
System.out.println("新连接:"+socket.getInetAddress()+":"+socket.getPort());
Thread.sleep(10000);
}catch(Exception e){e.printStackTrace();}finally{
try{
System.out.println("关闭连接:"+socket.getInetAddress()+":"+socket.getPort());
if(socket!=null)socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
当然是先多线程还有其它的方式,譬如线程池,或者JVM自带的线程池都可以。这里就不说明了。
Java读取、创建xml(通过dom方式)
创建一个接口
XmlInterface.java
publicinterfaceXmlInterface {
/** * 建立XML文档 * @param fileName 文件全路径名称 */ publicvoidcreateXml(String fileName); /** * 解析XML文档 * @param fileName 文件全路径名称 */ publicvoidparserXml(String fileName); } |
接口实现
XmlImpl.java
packagecom.test.xml;
importjava.io.FileNotFoundException; importjava.io.FileOutputStream; importjava.io.IOException; importjava.io.PrintWriter; importjavax.xml.parsers.DocumentBuilder; importjavax.xml.parsers.DocumentBuilderFactory; importjavax.xml.parsers.ParserConfigurationException; importjavax.xml.transform.OutputKeys; importjavax.xml.transform.Transformer; importjavax.xml.transform.TransformerConfigurationException; importjavax.xml.transform.TransformerException; importjavax.xml.transform.TransformerFactory; importjavax.xml.transform.dom.DOMSource; importjavax.xml.transform.stream.StreamResult; importorg.w3c.dom.Document; importorg.w3c.dom.Element; importorg.w3c.dom.Node; importorg.w3c.dom.NodeList; importorg.xml.sax.SAXException;
publicclassXmlImpl implementsXmlInterface{ privateDocument document;
publicvoidinit() { try{ DocumentBuilderFactory factory = DocumentBuilderFactory .newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); this.document = builder.newDocument(); } catch(ParserConfigurationException e) { System.out.println(e.getMessage()); } }
publicvoidcreateXml(String fileName) { Element root = this.document.createElement("scores"); this.document.appendChild(root); Element employee = this.document.createElement("employee"); Element name = this.document.createElement("name"); name.appendChild(this.document.createTextNode("wangchenyang")); employee.appendChild(name); Element sex = this.document.createElement("sex"); sex.appendChild(this.document.createTextNode("m")); employee.appendChild(sex); Element age = this.document.createElement("age"); age.appendChild(this.document.createTextNode("26")); employee.appendChild(age); root.appendChild(employee); TransformerFactory tf = TransformerFactory.newInstance(); try{ Transformer transformer = tf.newTransformer(); DOMSource source = newDOMSource(document); transformer.setOutputProperty(OutputKeys.ENCODING, "gb2312"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); PrintWriter pw = newPrintWriter(newFileOutputStream(fileName)); StreamResult result = newStreamResult(pw); transformer.transform(source, result); System.out.println("生成XML文件成功!"); } catch(TransformerConfigurationException e) { System.out.println(e.getMessage()); } catch(IllegalArgumentException e) { System.out.println(e.getMessage()); } catch(FileNotFoundException e) { System.out.println(e.getMessage()); } catch(TransformerException e) { System.out.println(e.getMessage()); } }
publicvoidparserXml(String fileName) { try{ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document document = db.parse(fileName);
NodeList employees = document.getChildNodes(); for(inti = 0; i < employees.getLength(); i++) { Node employee = employees.item(i); NodeList employeeInfo = employee.getChildNodes(); for(intj = 0; j < employeeInfo.getLength(); j++) { Node node = employeeInfo.item(j); NodeList employeeMeta = node.getChildNodes(); for(intk = 0; k < employeeMeta.getLength(); k++) { System.out.println(employeeMeta.item(k).getNodeName() + ":"+ employeeMeta.item(k).getTextContent()); } } } System.out.println("解析完毕"); } catch(FileNotFoundException e) { System.out.println(e.getMessage()); } catch(ParserConfigurationException e) { System.out.println(e.getMessage()); } catch(SAXException e) { System.out.println(e.getMessage()); } catch(IOException e) { System.out.println(e.getMessage()); } } } |
测试
publicclassMain {
publicstaticvoidmain(String args[]){ XmlImpl dd=newXmlImpl(); String str="D:/grade.xml"; dd.init(); dd.createXml(str); //创建xml dd.parserXml(str); //读取xml } } |
结果
生成xml
<?xml version="1.0" encoding="GB2312"?>
<scores>
<employee>
<name>wangchenyang</name>
<sex>m</sex>
<age>26</age>
</employee>
</scores>
读取xml
生成XML文件成功! #text:
name:wangchenyang #text:
sex:m #text:
age:26 #text:
解析完毕 |
javaweb学习总结(五)——Servlet开发(一)
一、Servlet简介
Servlet是sun公司提供的一门用于开发动态web资源的技术。
Sun公司在其API中提供了一个servlet接口,用户若想用发一个动态web资源(即开发一个Java程序向浏览器输出数据),需要完成以下2个步骤:
1、编写一个Java类,实现servlet接口。
2、把开发好的Java类部署到web服务器中。
按照一种约定俗成的称呼习惯,通常我们也把实现了servlet接口的java程序,称之为Servlet
二、Servlet的运行过程
Servlet程序是由WEB服务器调用,web服务器收到客户端的Servlet访问请求后:
①Web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第④步,否则,执行第②步。
②装载并创建该Servlet的一个实例对象。
③调用Servlet实例对象的init()方法。
④创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。
⑤WEB应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。
三、Servlet调用图
四、在Eclipse中开发Servlet
在eclipse中新建一个web project工程,eclipse会自动创建下图所示目录结构:
4.1、Servlet接口实现类
Servlet接口SUN公司定义了两个默认实现类,分别为:GenericServlet、HttpServlet。
HttpServlet指能够处理HTTP请求的servlet,它在原有Servlet接口上添加了一些与HTTP协议处理方法,它比Servlet接口的功能更为强大。因此开发人员在编写Servlet时,通常应继承这个类,而避免直接去实现Servlet接口。
HttpServlet在实现Servlet接口时,覆写了service方法,该方法体内的代码会自动判断用户的请求方式,如为GET请求,则调用HttpServlet的doGet方法,如为Post请求,则调用doPost方法。因此,开发人员在编写Servlet时,通常只需要覆写doGet或doPost方法,而不要去覆写service方法。
4.2、通过Eclipse创建和编写Servlet
选中gacl.servlet.study包,右键→New→Servlet,如下图所示:
这样,我们就通过Eclipse帮我们创建好一个名字为ServletDemo1的Servlet,创建好的ServletDemo01里面会有如下代码:
1packagegacl.servlet.study;23importjava.io.IOException;4importjava.io.PrintWriter;56importjavax.servlet.ServletException;7importjavax.servlet.http.HttpServlet;8importjavax.servlet.http.HttpServletRequest;9importjavax.servlet.http.HttpServletResponse;1011publicclassServletDemo1 extendsHttpServlet {1213/**14* The doGet method of the servlet. <br>15*16* This method is called when a form has its tag value method equals to get.17* 18* @paramrequest the request send by the client to the server19* @paramresponse the response send by the server to the client20* @throwsServletException if an error occurred21* @throwsIOException if an error occurred22*/23publicvoiddoGet(HttpServletRequest request, HttpServletResponse response)24throwsServletException, IOException {2526response.setContentType("text/html");27PrintWriter out = response.getWriter();28out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");29out.println("<HTML>");30out.println(" <HEAD><TITLE>A Servlet</TITLE></HEAD>");31out.println(" <BODY>");32out.print(" This is ");33out.print(this.getClass());34out.println(", using the GET method");35out.println(" </BODY>");36out.println("</HTML>");37out.flush();38out.close();39}4041/**42* The doPost method of the servlet. <br>43*44* This method is called when a form has its tag value method equals to post.45* 46* @paramrequest the request send by the client to the server47* @paramresponse the response send by the server to the client48* @throwsServletException if an error occurred49* @throwsIOException if an error occurred50*/51publicvoiddoPost(HttpServletRequest request, HttpServletResponse response)52throwsServletException, IOException {5354response.setContentType("text/html");55PrintWriter out = response.getWriter();56out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");57out.println("<HTML>");58out.println(" <HEAD><TITLE>A Servlet</TITLE></HEAD>");59out.println(" <BODY>");60out.print(" This is ");61out.print(this.getClass());62out.println(", using the POST method");63out.println(" </BODY>");64out.println("</HTML>");65out.flush();66out.close();67}6869}
这些代码都是Eclipse自动生成的,而web.xml文件中也多了<servlet></servlet>和<servlet-mapping></servlet-mapping>两对标签,这两对标签是配置ServletDemo1的,如下图所示:
然后我们就可以通过浏览器访问ServletDemo1这个Servlet,如下图所示:
五、Servlet开发注意细节
5.1、Servlet访问URL映射配置
由于客户端是通过URL地址访问web服务器中的资源,所以Servlet程序若想被外界访问,必须把servlet程序映射到一个URL地址上,这个工作在web.xml文件中使用<servlet>元素和<servlet-mapping>元素完成。
<servlet>元素用于注册Servlet,它包含有两个主要的子元素:<servlet-name>和<servlet-class>,分别用于设置Servlet的注册名称和Servlet的完整类名。
一个<servlet-mapping>元素用于映射一个已注册的Servlet的一个对外访问路径,它包含有两个子元素:<servlet-name>和<url-pattern>,分别用于指定Servlet的注册名称和Servlet的对外访问路径。例如:
1<servlet>2<servlet-name>ServletDemo1</servlet-name>3<servlet-class>gacl.servlet.study.ServletDemo1</servlet-class>4</servlet>56<servlet-mapping>7<servlet-name>ServletDemo1</servlet-name>8<url-pattern>/servlet/ServletDemo1</url-pattern>9</servlet-mapping>
同一个Servlet可以被映射到多个URL上,即多个<servlet-mapping>元素的<servlet-name>子元素的设置值可以是同一个Servlet的注册名。 例如:
1<servlet>2<servlet-name>ServletDemo1</servlet-name>3<servlet-class>gacl.servlet.study.ServletDemo1</servlet-class>4</servlet>56<servlet-mapping>7<servlet-name>ServletDemo1</servlet-name>8<url-pattern>/servlet/ServletDemo1</url-pattern>9</servlet-mapping>10<servlet-mapping>11<servlet-name>ServletDemo1</servlet-name>12<url-pattern>/1.htm</url-pattern>13</servlet-mapping>14<servlet-mapping>15<servlet-name>ServletDemo1</servlet-name>16<url-pattern>/2.jsp</url-pattern>17</servlet-mapping>18<servlet-mapping>19<servlet-name>ServletDemo1</servlet-name>20<url-pattern>/3.php</url-pattern>21</servlet-mapping>22<servlet-mapping>23<servlet-name>ServletDemo1</servlet-name>24<url-pattern>/4.ASPX</url-pattern>25</servlet-mapping>
通过上面的配置,当我们想访问名称是ServletDemo1的Servlet,可以使用如下的几个地址去访问:
http://localhost:8080/JavaWeb_Servlet_Study_20140531/servlet/ServletDemo1
http://localhost:8080/JavaWeb_Servlet_Study_20140531/1.htm
http://localhost:8080/JavaWeb_Servlet_Study_20140531/2.jsp
http://localhost:8080/JavaWeb_Servlet_Study_20140531/3.php
http://localhost:8080/JavaWeb_Servlet_Study_20140531/4.ASPX
ServletDemo1被映射到了多个URL上。
5.2、Servlet访问URL使用*通配符映射
在Servlet映射到的URL中也可以使用*通配符,但是只能有两种固定的格式:一种格式是"*.扩展名",另一种格式是以正斜杠(/)开头并以"/*"结尾。例如:
1<servlet>2<servlet-name>ServletDemo1</servlet-name>3<servlet-class>gacl.servlet.study.ServletDemo1</servlet-class>4</servlet>56<servlet-mapping>7<servlet-name>ServletDemo1</servlet-name>8<url-pattern>/*</url-pattern>
*可以匹配任意的字符,所以此时可以用任意的URL去访问ServletDemo1这个Servlet,如下图所示:
对于如下的一些映射关系:
Servlet1 映射到 /abc/*
Servlet2 映射到 /*
Servlet3 映射到 /abc
Servlet4 映射到 *.do
问题:
当请求URL为“/abc/a.html”,“/abc/*”和“/*”都匹配,哪个servlet响应
Servlet引擎将调用Servlet1。
当请求URL为“/abc”时,“/abc/*”和“/abc”都匹配,哪个servlet响应
Servlet引擎将调用Servlet3。
当请求URL为“/abc/a.do”时,“/abc/*”和“*.do”都匹配,哪个servlet响应
Servlet引擎将调用Servlet1。
当请求URL为“/a.do”时,“/*”和“*.do”都匹配,哪个servlet响应
Servlet引擎将调用Servlet2。
当请求URL为“/xxx/yyy/a.do”时,“/*”和“*.do”都匹配,哪个servlet响应
Servlet引擎将调用Servlet2。
匹配的原则就是"谁长得更像就找谁"
5.3、Servlet与普通Java类的区别
Servlet是一个供其他Java程序(Servlet引擎)调用的Java类,它不能独立运行,它的运行完全由Servlet引擎来控制和调度。
针对客户端的多次Servlet请求,通常情况下,服务器只会创建一个Servlet实例对象,也就是说Servlet实例对象一旦创建,它就会驻留在内存中,为后续的其它请求服务,直至web容器退出,servlet实例对象才会销毁。
在Servlet的整个生命周期内,Servlet的init方法只被调用一次。而对一个Servlet的每次访问请求都导致Servlet引擎调用一次servlet的service方法。对于每次访问请求,Servlet引擎都会创建一个新的HttpServletRequest请求对象和一个新的HttpServletResponse响应对象,然后将这两个对象作为参数传递给它调用的Servlet的service()方法,service方法再根据请求方式分别调用doXXX方法。
如果在<servlet>元素中配置了一个<load-on-startup>元素,那么WEB应用程序在启动时,就会装载并创建Servlet的实例对象、以及调用Servlet实例对象的init()方法。
举例:
<servlet>
<servlet-name>invoker</servlet-name>
<servlet-class>
org.apache.catalina.servlets.InvokerServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
用途:为web应用写一个InitServlet,这个servlet配置为启动时装载,为整个web应用创建必要的数据库表和数据。
5.4、缺省Servlet
如果某个Servlet的映射路径仅仅为一个正斜杠(/),那么这个Servlet就成为当前Web应用程序的缺省Servlet。
凡是在web.xml文件中找不到匹配的<servlet-mapping>元素的URL,它们的访问请求都将交给缺省Servlet处理,也就是说,缺省Servlet用于处理所有其他Servlet都不处理的访问请求。 例如:
1<servlet>2<servlet-name>ServletDemo2</servlet-name>3<servlet-class>gacl.servlet.study.ServletDemo2</servlet-class>4<load-on-startup>1</load-on-startup>5</servlet>67<!-- 将ServletDemo2配置成缺省Servlet -->8<servlet-mapping>9<servlet-name>ServletDemo2</servlet-name>10<url-pattern>/</url-pattern>11</servlet-mapping>
当访问不存在的Servlet时,就使用配置的默认Servlet进行处理,如下图所示:
在<tomcat的安装目录>\conf\web.xml文件中,注册了一个名称为org.apache.catalina.servlets.DefaultServlet的Servlet,并将这个Servlet设置为了缺省Servlet。
1<servlet>2<servlet-name>default</servlet-name>3<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>4<init-param>5<param-name>debug</param-name>6<param-value>0</param-value>7</init-param>8<init-param>9<param-name>listings</param-name>10<param-value>false</param-value>11</init-param>12<load-on-startup>1</load-on-startup>13</servlet>1415<!-- The mapping for the default servlet -->16<servlet-mapping>17<servlet-name>default</servlet-name>18<url-pattern>/</url-pattern>19</servlet-mapping>
当访问Tomcat服务器中的某个静态HTML文件和图片时,实际上是在访问这个缺省Servlet。
5.5、Servlet的线程安全问题
当多个客户端并发访问同一个Servlet时,web服务器会为每一个客户端的访问请求创建一个线程,并在这个线程上调用Servlet的service方法,因此service方法内如果访问了同一个资源的话,就有可能引发线程安全问题。例如下面的代码:
不存在线程安全问题的代码:
1packagegacl.servlet.study;23importjava.io.IOException;45importjavax.servlet.ServletException;6importjavax.servlet.http.HttpServlet;7importjavax.servlet.http.HttpServletRequest;8importjavax.servlet.http.HttpServletResponse;910publicclassServletDemo3 extendsHttpServlet {111213publicvoiddoGet(HttpServletRequest request, HttpServletResponse response)14throwsServletException, IOException {1516/**17* 当多线程并发访问这个方法里面的代码时,会存在线程安全问题吗18* i变量被多个线程并发访问,但是没有线程安全问题,因为i是doGet方法里面的局部变量,19* 当有多个线程并发访问doGet方法时,每一个线程里面都有自己的i变量,20* 各个线程操作的都是自己的i变量,所以不存在线程安全问题21* 多线程并发访问某一个方法的时候,如果在方法内部定义了一些资源(变量,集合等)22* 那么每一个线程都有这些东西,所以就不存在线程安全问题了23*/24inti=1;25i++;26response.getWriter().write(i);27}2829publicvoiddoPost(HttpServletRequest request, HttpServletResponse response)30throwsServletException, IOException {31doGet(request, response);32}3334}
存在线程安全问题的代码:
1packagegacl.servlet.study;23importjava.io.IOException;45importjavax.servlet.ServletException;6importjavax.servlet.http.HttpServlet;7importjavax.servlet.http.HttpServletRequest;8importjavax.servlet.http.HttpServletResponse;910publicclassServletDemo3 extendsHttpServlet {1112inti=1;13publicvoiddoGet(HttpServletRequest request, HttpServletResponse response)14throwsServletException, IOException {1516i++;17try{18Thread.sleep(1000*4);19} catch(InterruptedException e) {20e.printStackTrace();21}22response.getWriter().write(i+"");23}2425publicvoiddoPost(HttpServletRequest request, HttpServletResponse response)26throwsServletException, IOException {27doGet(request, response);28}2930}
把i定义成全局变量,当多个线程并发访问变量i时,就会存在线程安全问题了,如下图所示:同时开启两个浏览器模拟并发访问同一个Servlet,本来正常来说,第一个浏览器应该看到2,而第二个浏览器应该看到3的,结果两个浏览器都看到了3,这就不正常。
线程安全问题只存在多个线程并发操作同一个资源的情况下,所以在编写Servlet的时候,如果并发访问某一个资源(变量,集合等),就会存在线程安全问题,那么该如何解决这个问题呢?
先看看下面的代码:
1packagegacl.servlet.study;23importjava.io.IOException;45importjavax.servlet.ServletException;6importjavax.servlet.http.HttpServlet;7importjavax.servlet.http.HttpServletRequest;8importjavax.servlet.http.HttpServletResponse;91011publicclassServletDemo3 extendsHttpServlet {1213inti=1;14publicvoiddoGet(HttpServletRequest request, HttpServletResponse response)15throwsServletException, IOException {16/**17* 加了synchronized后,并发访问i时就不存在线程安全问题了,18* 为什么加了synchronized后就没有线程安全问题了呢?19* 假如现在有一个线程访问Servlet对象,那么它就先拿到了Servlet对象的那把锁20* 等到它执行完之后才会把锁还给Servlet对象,由于是它先拿到了Servlet对象的那把锁,21* 所以当有别的线程来访问这个Servlet对象时,由于锁已经被之前的线程拿走了,后面的线程只能排队等候了22* 23*/24synchronized(this) {//在java中,每一个对象都有一把锁,这里的this指的就是Servlet对象25i++;26try{27Thread.sleep(1000*4);28} catch(InterruptedException e) {29e.printStackTrace();30}31response.getWriter().write(i+"");32}3334}3536publicvoiddoPost(HttpServletRequest request, HttpServletResponse response)37throwsServletException, IOException {38doGet(request, response);39}4041}
现在这种做法是给Servlet对象加了一把锁,保证任何时候都只有一个线程在访问该Servlet对象里面的资源,这样就不存在线程安全问题了,如下图所示:
这种做法虽然解决了线程安全问题,但是编写Servlet却万万不能用这种方式处理线程安全问题,假如有9999个人同时访问这个Servlet,那么这9999个人必须按先后顺序排队轮流访问。
针对Servlet的线程安全问题,Sun公司是提供有解决方案的:让Servlet去实现一个SingleThreadModel接口,如果某个Servlet实现了SingleThreadModel接口,那么Servlet引擎将以单线程模式来调用其service方法。
查看Sevlet的API可以看到,SingleThreadModel接口中没有定义任何方法和常量,在Java中,把没有定义任何方法和常量的接口称之为标记接口,经常看到的一个最典型的标记接口就是"Serializable",这个接口也是没有定义任何方法和常量的,标记接口在Java中有什么用呢?主要作用就是给某个对象打上一个标志,告诉JVM,这个对象可以做什么,比如实现了"Serializable"接口的类的对象就可以被序列化,还有一个"Cloneable"接口,这个也是一个标记接口,在默认情况下,Java中的对象是不允许被克隆的,就像现实生活中的人一样,不允许克隆,但是只要实现了"Cloneable"接口,那么对象就可以被克隆了。
让Servlet实现了SingleThreadModel接口,只要在Servlet类的定义中增加实现SingleThreadModel接口的声明即可。
对于实现了SingleThreadModel接口的Servlet,Servlet引擎仍然支持对该Servlet的多线程并发访问,其采用的方式是产生多个Servlet实例对象,并发的每个线程分别调用一个独立的Servlet实例对象。
实现SingleThreadModel接口并不能真正解决Servlet的线程安全问题,因为Servlet引擎会创建多个Servlet实例对象,而真正意义上解决多线程安全问题是指一个Servlet实例对象被多个线程同时调用的问题。事实上,在Servlet API 2.4中,已经将SingleThreadModel标记为Deprecated(过时的)。
Servlet生命周期与工作原理
Servlet生命周期分为三个阶段:
1,初始化阶段调用init()方法
2,响应客户请求阶段 调用service()方法
3,终止阶段 调用destroy()方法
Servlet初始化阶段:
在下列时刻Servlet容器装载Servlet:
1,Servlet容器启动时自动装载某些Servlet,实现它只需要在web.XML文件中的<Servlet></Servlet>之间添加如下代码:
<loadon-startup>1</loadon-startup> |
2,在Servlet容器启动后,客户首次向Servlet发送请求
3,Servlet类文件被更新后,重新装载Servlet
Servlet被装载后,Servlet容器创建一个Servlet实例并且调用Servlet的init()方法进行初始化。在Servlet的整个生命周期内,init()方法只被调用一次。
Servlet工作原理:
首先简单解释一下Servlet接收和响应客户请求的过程,首先客户发送一个请求,Servlet是调用service()方法对请求进行响应的,通过源代码可见,service()方法中对请求的方式进行了匹配,选择调用doGet,doPost等这些方法,然后再进入对应的方法中调用逻辑层的方法,实现对客户的响应。在Servlet接口和GenericServlet中是没有doGet,doPost等等这些方法的,HttpServlet中定义了这些方法,但是都是返回error信息,所以,我们每次定义一个Servlet的时候,都必须实现doGet或doPost等这些方法。
每一个自定义的Servlet都必须实现Servlet的接口,Servlet接口中定义了五个方法,其中比较重要的三个方法涉及到Servlet的生命周期,分别是上文提到的init(),service(),destroy()方法。GenericServlet是一个通用的,不特定于任何协议的Servlet,它实现了Servlet接口。而HttpServlet继承于GenericServlet,因此HttpServlet也实现了Servlet接口。所以我们定义Servlet的时候只需要继承HttpServlet即可。
Servlet接口和GenericServlet是不特定于任何协议的,而HttpServlet是特定于HTTP协议的类,所以HttpServlet中实现了service()方法,并将请求ServletRequest,ServletResponse强转为HttpRequest和HttpResponse。
publicvoidservice(ServletRequest req,ServletResponse res) throwsServletException,IOException { HttpRequest request; HttpResponse response;
try { req = (HttpRequest)request; res = (HttpResponse)response; }catch(ClassCastException e) { thrownewServletException("non-HTTP request response"); } service(request,response); } |
代码的最后调用了HTTPServlet自己的service(request,response)方法,然后根据请求去调用对应的doXXX方法,因为HttpServlet中的doXXX方法都是返回错误信息,
protectedvoiddoGet(HttpServletRequest res,HttpServletResponse resp) throwsServletException,IOException { String protocol = req.getProtocol(); String msg = IStrings.getString("http.method_get_not_supported"); if(protocol.equals("1.1")) { resp.sendError(HttpServletResponse.SC.METHOD.NOT.ALLOWED,msg); } esle { resp.sendError(HttpServletResponse.SC_BAD_REQUEST,msg); } } |
所以需要我们在自定义的Servlet中override这些方法!
源码面前,了无秘密!
---------------------------------------------------------------------------------------------------------------------------------
Servlet响应请求阶段:
对于用户到达Servlet的请求,Servlet容器会创建特定于这个请求的ServletRequest对象和ServletResponse对象,然后调用Servlet的service方法。service方法从ServletRequest对象获得客户请求信息,处理该请求,并通过ServletResponse对象向客户返回响应信息。
对于Tomcat来说,它会将传递过来的参数放在一个Hashtable中,该Hashtable的定义是:
privateHashtable<String String[]> paramHashStringArray = newHashtable<String String[]>(); |
这是一个String-->String[]的键值映射。
HashMap线程不安全的,Hashtable线程安全。
-----------------------------------------------------------------------------------------------------------------------------------
Servlet终止阶段:
当WEB应用被终止,或Servlet容器终止运行,或Servlet容器重新装载Servlet新实例时,Servlet容器会先调用Servlet的destroy()方法,在destroy()方法中可以释放掉Servlet所占用的资源。
-----------------------------------------------------------------------------------------------------------------------------------
Servlet何时被创建:
1,默认情况下,当WEB客户第一次请求访问某个Servlet的时候,WEB容器将创建这个Servlet的实例。
2,当web.xml文件中如果<servlet>元素中指定了<load-on-startup>子元素时,Servlet容器在启动web服务器时,将按照顺序创建并初始化Servlet对象。
注意:在web.xml文件中,某些Servlet只有<serlvet>元素,没有<servlet-mapping>元素,这样我们无法通过url的方式访问这些Servlet,这种Servlet通常会在<servlet>元素中配置一个<load-on-startup>子元素,让容器在启动的时候自动加载这些Servlet并调用init()方法,完成一些全局性的初始化工作。
Web应用何时被启动:
1,当Servlet容器启动的时候,所有的Web应用都会被启动
2,控制器启动web应用
-----------------------------------------------------------------------------------------------------------------------------------------------
Servlet与JSP的比较:
有许多相似之处,都可以生成动态网页。
JSP的优点是擅长于网页制作,生成动态页面比较直观,缺点是不容易跟踪与排错。
Servlet是纯Java语言,擅长于处理流程和业务逻辑,缺点是生成动态网页不直观。
一 Servlet的生命周期
Servlet是运行在Servlet容器(有时候也叫引擎,是基于服务器和应用程序服务器的一部分,用于在发送的请求和响应之上提供网络服务,解码基于MIME的请求,格式化基于MIME的响应。常用的Tomcat,Jboss,weblogic都是servlet容器)中的,其生命周期由容器来管理。Servlet的生命周期通过java.servlet.Servlet接口中的init(),service()和destory()
方法表示。Servlet的生命周期有四个阶段:加载并实例化,初始化,请求处理,销毁。
1.加载并实例化
Servlet容器负责加载和实例化Servlet。当Servlet容器启动时,或者在容器检测到需要这个Servlet来响应第一个请求时,创建Servlet实例。当Servlet容器启动后,Servlet通过类加载器来加载Servlet类,加载完成后再new一个Servlet对象来完成实例化。类文件被更新后,也会重新装载Servlet。
2.初始化
在Servlet实例化之后,容器将调用init()方法,并传递实现ServletConfig接口的对象。在init()方法中,Servlet可以从web.xml中读取配置参数,或者执行任何其他一次性活动,在Servlet的整个生命周期,init()方法只被调用一次。
3.请求处理
当Servlet初始化之后,容器就可以准备处理客户端的请求了。当容器收到对这一Servlet的请求,就调用Servlet的service()方法,并把请求和响应对象,作为参数传递。当并行的请求到来时,多个service()方法能够同时运行在 独立的线程中。通过分析ServletRequest对象或者HttpServletRequest对象,service()方法处理用户的请求,并调用ServletResponse或者HttpServletResponse对象来响应。
4.销毁
一旦Servlet容器检测到一个Servlet要卸载,这可能是因为要回收资源或者因为它正在被关闭,容易会在所有的Servlet的service()线程之后,调用destory()方法。然后,Servlet就可以进行无用存储单元收集清理。这样Servlet就被销毁了。这四个阶段共同决定了Servlet的生命周期。
二 JSP的生命周期
JSP页面必须转换成Servlet,才能对请求进行服务,因此JSP的底层完全是Servlet。这样看来JSP 的生命周期就包括六个阶段:转换,编译,加载并实例化,初始化(_jspInit),请求处理(_jspService()调用),销毁(_jspDestory())。
转换:就是web容器将JSP文件转换成一个包含了Servlet类定义的java源文件。
编译:把在转换阶段创建的java源文件变异成类文件。
JSP 生命周期其他的四个阶段跟Servlet生命周期相同。
Java中的EL和JSTl
一.EL
EL语法
1.以“${”作为开始,以“}”作为结束
直接使用变量名获取值$
${ username }
2.变量属性范围名称
page |
pageScope,例如${pageScope.username},表示在page范围内查找username变量,找不到返回Null |
request |
requstScope |
session |
sessionScope |
application |
applicationScope |
3.EL隐式对象
作用域访问对象 1.PageScope 2.RequestScope 3.SessionScope 4. ApplicationScope
参数访问对象 1.param 2.paramValues
jsp隐式对象 1.pagecontext
RequestScope的使用:
String name=(String)session.setAttribute("user",user);
在会员登入后将user对象保存到session中
在.jsp使用EL获取是就可以使用
欢迎您:${sessionScope.user.name}
其它作用域对象使用类似
Param的使用:
在一个登陆提交页面提交后,接受的页面可以这样接受参数
用户名:${param.uid}
密码:${param.pass}
Param获取的是单个的参数
Paramvalues的使用:
兴趣爱好:${paramValues.ch[0]},${paramValues.ch[1]},${paramValues.ch[2]}
Paramvalues获取的是一个集合
Pagecontext的使用:
请求的IP:${pageContext.request.remoteAddr}
Pagecontext可以获取到请求里包含的信息
二.JSTL
EL中不存在逻辑处理,JSTL实现JSP页面中逻辑处理,所有两者需要联合使用
在JSTL的使用前需要在网页顶部加入下面内容
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
(可能导入了这句话还是使用不了JSTL,这可能和版本有关,解决办法是在WEB-INFO目录下的lib导入两个jar包,网上可以下,我这里也有我的百度网盘的下载地址链接http://pan.baidu.com/share/link?shareid=64504&uk=909074031)
通用标签 set out remove
条件标签库 if
迭代标签库 foreach
通用标签其实一般都用的很少,主要使用的还是条件标签和迭代标签
通用标签
Set的使用
<c:set var= "example" value="${100+1}" scope="session" />
Out的使用
<c:out value="${example}"/>
Remove的使用
<c:remove var= "example" scope="session"/>
条件标签
<c:if test="codition" var="name" scope="applicationArea" >
条件符合时执行的代码
</c:if>
Condition是判断的条件
Name是判断条件的结果是true或false
Scope是作用域的范围
如下:
<c:iftest="${user==null}"var="isLogin"></c:if>
判断登陆时是否获取到了user对象的结果,如果为null的话islogin的值为true
迭代标签
<c:forEach items=collection
var="varName" varStatus="vatStatusName" begin="start" end="end" step="count">
…循环体代码…
</forEach>
Items是要遍历的集合对象
Var集合内数据的名称
Varstatus指定var成员的信息
案例:
<c:forEachitems="${requestScope.pagelist}"var="news"varStatus="status">
<li>${news.title}<span>时间:${news.publictime}<ahref='ManageNewsServlet?type=update&id=${news.newsid}'>修改</a>
</span></li>
每遍历5条数据后就间隔一个<li class='space'></li>
Status.index是当前变量的索引值
<c:iftest="$(status.index%5==0 && status.index>0)">
<liclass='space'></li>
</c:if>
</c:forEach>
JavaWeb过滤器.监听器.拦截器-原理&区别-个人总结(转)
1、拦截器是基于java的反射机制的,而过滤器是基于函数回调
2、过滤器依赖与servlet容器,而拦截器不依赖与servlet容器
3、拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用
4、拦截器可以访问action上下文、值栈里的对象,而过滤器不能
5、在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次
拦截器 :是在面向切面编程的就是在你的service或者一个方法,前调用一个方法,或者在方法后调用一个方法比如动态代理就是拦截器的简单实现,在你调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在你调用方法后打印出字符串,甚至在你抛出异常的时候做业务逻辑的操作。
1.Struts2拦截器是在访问某个Action或Action的某个方法,字段之前或之后实施拦截,并且Struts2拦截器是可插拔的,拦截器是AOP的一种实现。
2. 拦截器栈(Interceptor Stack)。Struts2拦截器栈就是将拦截器按一定的顺序联结成一条链。在访问被拦截的方法或字段时,Struts2拦截器链中的拦截器就会按其之前定义的顺序被调用。
附:面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。
但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。
AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。
下面通过实例来看一下过滤器和拦截器的区别:
使用拦截器进行/admin 目录下jsp页面的过滤
<package name="newsDemo" extends="struts-default"
namespace="/admin">
<interceptors>
<interceptor name="auth" class="com.test.news.util.AccessInterceptor" />
<interceptor-stack name="authStack">
<interceptor-ref name="auth" />
</interceptor-stack>
</interceptors>
<!-- action -->
<action name="newsAdminView!*" class="newsAction"
method="{1}">
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="authStack">
</interceptor-ref>
下面是我实现的Interceptor class:
package com.test.news.util;
import java.util.Map;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import com.test.news.action.AdminLoginAction;
/**
* @author chaoyin */
public class AccessInterceptor extends AbstractInterceptor {
private static final long serialVersionUID = -4291195782860785705L;
@Override
public String intercept(ActionInvocation actionInvocation) throws Exception {
ActionContext actionContext = actionInvocation.getInvocationContext();
Map session = actionContext.getSession();
//except login action
Object action = actionInvocation.getAction();
if (action instanceof AdminLoginAction) {
return actionInvocation.invoke();
}
//check session
if(session.get("user")==null ){
return "logout";
}
return actionInvocation.invoke();//go on
}
}
过滤器:是在java web中,你传入的request,response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者struts的 action进行业务逻辑,比如过滤掉非法url(不是login.do的地址请求,如果用户没有登陆都过滤掉),或者在传入servlet或者struts的action前统一设置字符集,或者去除掉一些非法字符。主要为了减轻服务器负载,减少压力。
使用过滤器进行/admin 目录下jsp页面的过滤,首先在web.xml进行过滤器配置:
<filter>
<filter-name>access filter</filter-name>
<filter-class>
com.test.news.util.AccessFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>access filter</filter-name>
<url-pattern>/admin/*</url-pattern>
</filter-mapping>
下面是过滤的实现类:
package com.test.news.util;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class AccessFilter implements Filter {
/**
* @author chaoyin
*/
public void destroy() {
}
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)arg0;
HttpServletResponse response = (HttpServletResponse)arg1;
HttpSession session = request.getSession();
if(session.getAttribute("user")== null && request.getRequestURI().indexOf("login.jsp")==-1 ){
response.sendRedirect("login.jsp");
return ;
}
filterChain.doFilter(arg0, arg1);
}
public void init(FilterConfig arg0) throws ServletException {
}
}
编程改变生活,改变人生
首先来看一下Servlet的过滤器内容:
一、Servlet过滤器的概念:
***************************************************************************************
Servlet过滤器是在Java Servlet规范2.3中定义的,它能够对Servlet容器的请求和响应对象进行检查和修改。
Servlet过滤器本身并不产生请求和响应对象,它只能提供过滤作用。Servlet过期能够在Servlet被调用之前检查Request对象,修改Request Header和Request内容;在Servlet被调用之后检查Response对象,修改Response Header和Response内容。
Servlet过期负责过滤的Web组件可以是Servlet、JSP或者HTML文件。
***************************************************************************************
二、Servlet过滤器的特点:
***************************************************************************************
A.Servlet过滤器可以检查和修改ServletRequest和ServletResponse对象
B.Servlet过滤器可以被指定和特定的URL关联,只有当客户请求访问该URL时,才会触发过滤器
C.Servlet过滤器可以被串联在一起,形成管道效应,协同修改请求和响应对象
***************************************************************************************
三、Servlet过滤器的作用:
***************************************************************************************
A.查询请求并作出相应的行动。
B.阻塞请求-响应对,使其不能进一步传递。
C.修改请求的头部和数据。用户可以提供自定义的请求。
D.修改响应的头部和数据。用户可以通过提供定制的响应版本实现。
E.与外部资源进行交互。
***************************************************************************************
四、Servlet过滤器的适用场合:
***************************************************************************************
A.认证过滤
B.登录和审核过滤
C.图像转换过滤
D.数据压缩过滤
E.加密过滤
F.令牌过滤
G.资源访问触发事件过滤
H.XSL/T过滤
I.Mime-type过滤
***************************************************************************************
五、Servlet过滤器接口的构成:
***************************************************************************************
所有的Servlet过滤器类都必须实现javax.servlet.Filter接口。这个接口含有3个过滤器类必须实现的方法:
A.init(FilterConfig):
这是Servlet过滤器的初始化方法,Servlet容器创建Servlet过滤器实例后将调用这个方法。在这个方法中可以读取web.xml文件中Servlet过滤器的初始化参数
B.doFilter(ServletRequest,ServletResponse,FilterChain):
这个方法完成实际的过滤操作,当客户请求访问于过滤器关联的URL时,Servlet容器将先调用过滤器的doFilter方法。FilterChain参数用于访问后续过滤器
C.destroy():
Servlet容器在销毁过滤器实例前调用该方法,这个方法中可以释放Servlet过滤器占用的资源
***************************************************************************************
六、Servlet过滤器的创建步骤:
***************************************************************************************
A.实现javax.servlet.Filter接口
B.实现init方法,读取过滤器的初始化函数
C.实现doFilter方法,完成对请求或过滤的响应
D.调用FilterChain接口对象的doFilter方法,向后续的过滤器传递请求或响应
E.销毁过滤器
***************************************************************************************
七、Servlet过滤器对请求的过滤:
***************************************************************************************
A.Servlet容器创建一个过滤器实例
B.过滤器实例调用init方法,读取过滤器的初始化参数
C.过滤器实例调用doFilter方法,根据初始化参数的值判断该请求是否合法
D.如果该请求不合法则阻塞该请求
E.如果该请求合法则调用chain.doFilter方法将该请求向后续传递
***************************************************************************************
八、Servlet过滤器对响应的过滤:
***************************************************************************************
A.过滤器截获客户端的请求
B.重新封装ServletResponse,在封装后的ServletResponse中提供用户自定义的输出流
C.将请求向后续传递
D.Web组件产生响应
E.从封装后的ServletResponse中获取用户自定义的输出流
F.将响应内容通过用户自定义的输出流写入到缓冲流中
G.在缓冲流中修改响应的内容后清空缓冲流,输出响应内容
***************************************************************************************
九、Servlet过滤器的发布:
***************************************************************************************
A.发布Servlet过滤器时,必须在web.xml文件中加入<filter>元素和<filter-mapping>元素。
B.<filter>元素用来定义一个过滤器:
属性 含义
filter-name 指定过滤器的名字
filter-class 指定过滤器的类名
init-param 为过滤器实例提供初始化参数,可以有多个
C.<filter-mapping>元素用于将过滤器和URL关联:
属性 含义
filter-name 指定过滤器的名字
url-pattern 指定和过滤器关联的URL,为”/*”表示所有URL
***************************************************************************************
十一、Servlet过滤器使用的注意事项
***************************************************************************************
A.由于Filter、FilterConfig、FilterChain都是位于javax.servlet包下,并非HTTP包所特有的,所以其中所用到的请求、响应对象ServletRequest、ServletResponse在使用前都必须先转换成HttpServletRequest、HttpServletResponse再进行下一步操作。
B.在web.xml中配置Servlet和Servlet过滤器,应该先声明过滤器元素,再声明Servlet元素
C.如果要在Servlet中观察过滤器生成的日志,应该确保在server.xml的localhost对应的<host>元素中配置如下<logger>元素:
<Logger className = “org.apache.catalina.logger.FileLogger”
directory = “logs”prefix = “localhost_log.”suffix=”.txt”
timestamp = “true”/>
***************************************************************************************
十二、一个实例
首先来看一下web.xml的配置:
[html]view plaincopy
1.<!--请求url日志记录过滤器-->
2.<filter>
3.<filter-name>logfilter</filter-name>
4.<filter-class>com.weijia.filterservlet.LogFilter</filter-class>
5.</filter>
6.<filter-mapping>
7.<filter-name>logfilter</filter-name>
8.<url-pattern>/*</url-pattern>
9.</filter-mapping>
10.
11.<!--编码过滤器-->
12.<filter>
13.<filter-name>setCharacterEncoding</filter-name>
14.<filter-class>com.weijia.filterservlet.EncodingFilter</filter-class>
15.<init-param>
16.<param-name>encoding</param-name>
17.<param-value>utf-8</param-value>
18.</init-param>
19.</filter>
20.<filter-mapping>
21.<filter-name>setCharacterEncoding</filter-name>
22.<url-pattern>/*</url-pattern>
23.</filter-mapping>
然后看一下编码过滤器:
[java]view plaincopy
1.packagecom.weijia.filterservlet;
2.
3.importjava.io.IOException;
4.importjava.util.Enumeration;
5.importjava.util.HashMap;
6.
7.importjavax.servlet.Filter;
8.importjavax.servlet.FilterChain;
9.importjavax.servlet.FilterConfig;
10.importjavax.servlet.ServletException;
11.importjavax.servlet.ServletRequest;
12.importjavax.servlet.ServletResponse;
13.
14.publicclassEncodingFilterimplementsFilter{
15.privateStringencoding;
16.privateHashMap<String,String>params=newHashMap<String,String>();
17.//项目结束时就已经进行销毁
18.publicvoiddestroy(){
19.System.out.println("enddotheencodingfilter!");
20.params=null;
21.encoding=null;
22.}
23.publicvoiddoFilter(ServletRequestreq,ServletResponseresp,FilterChainchain)throwsIOException,ServletException{
24.System.out.println("beforeencoding"+encoding+"filter!");
25.req.setCharacterEncoding(encoding);
26.chain.doFilter(req,resp);
27.System.out.println("afterencoding"+encoding+"filter!");
28.System.err.println("----------------------------------------");
29.}
30.
31.//项目启动时就已经进行读取
32.publicvoidinit(FilterConfigconfig)throwsServletException{
33.System.out.println("begindotheencodingfilter!");
34.encoding=config.getInitParameter("encoding");
35.for(Enumeration<?>e=config.getInitParameterNames();e.hasMoreElements();){
36.Stringname=(String)e.nextElement();
37.Stringvalue=config.getInitParameter(name);
38.params.put(name,value);
39.}
40.}
41.}
日志过滤器:
[java]view plaincopy
1.packagecom.weijia.filterservlet;
2.
3.importjava.io.IOException;
4.
5.importjavax.servlet.Filter;
6.importjavax.servlet.FilterChain;
7.importjavax.servlet.FilterConfig;
8.importjavax.servlet.ServletException;
9.importjavax.servlet.ServletRequest;
10.importjavax.servlet.ServletResponse;
11.importjavax.servlet.http.HttpServletRequest;
12.
13.publicclassLogFilterimplementsFilter{
14.
15.publicFilterConfigconfig;
16.
17.publicvoiddestroy(){
18.this.config=null;
19.System.out.println("enddotheloggingfilter!");
20.}
21.
22.publicvoiddoFilter(ServletRequestreq,ServletResponseres,FilterChainchain)throwsIOException,ServletException{
23.System.out.println("beforethelogfilter!");
24.//将请求转换成HttpServletRequest请求
25.HttpServletRequesthreq=(HttpServletRequest)req;
26.//记录日志
27.System.out.println("LogFilter已经截获到用户的请求的地址:"+hreq.getServletPath());
28.try{
29.//Filter只是链式处理,请求依然转发到目的地址。
30.chain.doFilter(req,res);
31.}catch(Exceptione){
32.e.printStackTrace();
33.}
34.System.out.println("afterthelogfilter!");
35.}
36.
37.publicvoidinit(FilterConfigconfig)throwsServletException{
38.System.out.println("begindothelogfilter!");
39.this.config=config;
40.}
41.
42.}
测试Servlet:
[java]view plaincopy
1.packagecom.weijia.filterservlet;
2.
3.importjava.io.IOException;
4.
5.importjavax.servlet.ServletException;
6.importjavax.servlet.http.HttpServlet;
7.importjavax.servlet.http.HttpServletRequest;
8.importjavax.servlet.http.HttpServletResponse;
9.
10.publicclassFilterServletextendsHttpServlet{
11.
12.privatestaticfinallongserialVersionUID=1L;
13.
14.publicvoiddoGet(HttpServletRequestrequest,HttpServletResponseresponse)
15.throwsServletException,IOException{
16.response.setDateHeader("expires",-1);
17.}
18.
19.publicvoiddoPost(HttpServletRequestrequest,HttpServletResponseresponse)
20.throwsServletException,IOException{
21.}
22.
23.}
访问FilterServlet
运行结果:
before the log filter!
Log Filter已经截获到用户的请求的地址:/FilterServlet
before encoding utf-8 filter!
after encoding utf-8 filter!
----------------------------------------
after the log filter!
我们从运行结果可以看到这个过滤器的调用关系:
类似于C++中的构造函数和析构函数的调用顺序,
这里我们在web.xml中注册的是先注册日志过滤器的,然后再注册
当我们重新部署应用的时候发现:
会先销毁上次的过滤器,然后再重新注册一下
下面在来看一下Servlet的监听器
Servlet监听器用于监听一些重要事件的发生,监听器对象可以在事情发生前、发生后可以做一些必要的处理。下面将介绍几种常用的监听器,以及它们都适合运用于那些环境。
分类及介绍:
1. ServletContextListener:用于监听WEB 应用启动和销毁的事件,监听器类需要实现javax.servlet.ServletContextListener 接口。
Java代码
1.publicclassQuartzListenerimplementsServletContextListener{
2.
3.privateLoggerlogger=LoggerFactory.getLogger(QuartzListener.class);
4.
5.publicvoidcontextInitialized(ServletContextEventsce){
6.
7.}
8.
9./**
10.*在服务器停止运行的时候停止所有的定时任务
11.*/
12.@SuppressWarnings("unchecked")
13.publicvoidcontextDestroyed(ServletContextEventarg0){
14.try{
15.Schedulerscheduler=StdSchedulerFactory.getDefaultScheduler();
16.List<JobExecutionContext>jobList=scheduler.getCurrentlyExecutingJobs();
17.for(JobExecutionContextjobContext:jobList){
18.Jobjob=jobContext.getJobInstance();
19.if(jobinstanceofInterruptableJob){
20.((InterruptableJob)job).interrupt();
21.}
22.}
23.scheduler.shutdown();
24.}catch(SchedulerExceptione){
25.logger.error("shutdownschedulerhappenederror",e);
26.}
27.}
28.}
2. ServletContextAttributeListener:用于监听WEB应用属性改变的事件,包括:增加属性、删除属性、修改属性,监听器类需要实现javax.servlet.ServletContextAttributeListener接口。
3. HttpSessionListener:用于监听Session对象的创建和销毁,监听器类需要实现javax.servlet.http.HttpSessionListener接口或者javax.servlet.http.HttpSessionActivationListener接口,或者两个都实现。
Java代码
1./**
2.*
3.*会话监听器
4.*<p/>
5.*
6.*/
7.publicclassSessionListenerimplementsHttpSessionListener{
8.
9.@Override
10.publicvoidsessionCreated(HttpSessionEventarg0){
11.
12.}
13.
14.@Override
15.publicvoidsessionDestroyed(HttpSessionEventevent){
16.HttpSessionsession=event.getSession();
17.Useruser=(BrsSession)session.getAttribute("currUser");
18.if(user!=null){
19.//TODOsomething
20.}
21.}
22.
23.}
4. HttpSessionActivationListener:用于监听Session对象的钝化/活化事件,监听器类需要实现javax.servlet.http.HttpSessionListener接口或者javax.servlet.http.HttpSessionActivationListener接口,或者两个都实现。
5. HttpSessionAttributeListener:用于监听Session对象属性的改变事件,监听器类需要实现javax.servlet.http.HttpSessionAttributeListener接口。
部署:
监听器的部署在web.xml文件中配置,在配置文件中,它的位置应该在过滤器的后面Servlet的前面
web.xml配置文件:
Java代码
1.<!--Quartz监听器-->
2.<listener>
3.<listener-class>
4.com.flyer.lisenter.QuartzListener
5.</listener-class>
6.</listener>
7.
Spring IOC容器基本原理aop
2.2.1 IOC容器的概念
IOC容器就是具有依赖注入功能的容器,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中new相关的对象,应用程序由IOC容器进行组装。在Spring中BeanFactory是IOC容器的实际代表者。
Spring IOC容器如何知道哪些是它管理的对象呢?这就需要配置文件,Spring IOC容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化及装配。一般使用基于xml配置文件进行配置元数据,而且Spring与配置文件完全解耦的,可以使用其他任何可能的方式进行配置元数据,比如注解、基于java文件的、基于属性文件的配置都可以。
那Spring IOC容器管理的对象叫什么呢?
2.2.2 Bean的概念
由IOC容器管理的那些组成你应用程序的对象我们就叫它Bean, Bean就是由Spring容器初始化、装配及管理的对象,除此之外,bean就与应用程序中的其他对象没有什么区别了。那IOC怎样确定如何实例化Bean、管理Bean之间的依赖关系以及管理Bean呢?这就需要配置元数据,在Spring中由BeanDefinition代表,后边会详细介绍,配置元数据指定如何实例化Bean、如何组装Bean等。概念知道的差不多了,让我们来做个简单的例子。
2.2.3 开始Spring Hello World之旅
1、准备需要的jar包
核心jar包:从下载的spring-framework-3.0.5.RELEASE-with-docs.zip中dist目录查找如下jar包
org.springframework.asm-3.0.5.RELEASE.jar
org.springframework.core-3.0.5.RELEASE.jar
org.springframework.beans-3.0.5.RELEASE.jar
org.springframework.context-3.0.5.RELEASE.jar
org.springframework.expression-3.0.5.RELEASE.jar
依赖的jar包:从下载的spring-framework-3.0.5.RELEASE-dependencies.zip中查找如下依赖jar包
com.springsource.org.apache.log4j-1.2.15.jar
com.springsource.org.apache.commons.logging-1.1.1.jar
com.springsource.org.apache.commons.collections-3.2.1.jar
2、创建标准Java工程:
3、项目搭建好了,让我们来开发接口,此处我们只需实现打印“Hello World!”,所以我们定义一个“sayHello”接口,代码如下:
packagecom.ljq.test;
publicinterfaceHelloService {
publicvoidsayHello();
}
4、接口开发好了,让我们来通过实现接口来完成打印“Hello World!”功能;
packagecom.ljq.test;
publicclassHelloServiceImpl implementsHelloService{
publicvoidsayHello(){
System.out.println("Hello World!");
}
}
5、接口和实现都开发好了,那如何使用Spring IOC容器来管理它们呢?这就需要配置文件,让IOC容器知道要管理哪些对象。让我们来看下配置文件helloworld.xml(放到src目录下):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation=" http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- id 表示组件的名字,class表示组件类 -->
<bean id="helloService" class="com.ljq.test.HelloServiceImpl" />
</beans>
6、现在万一具备,那如何获取IOC容器并完成我们需要的功能呢?首先应该实例化一个IOC容器,然后从容器中获取需要的对象,然后调用接口完成我们需要的功能,代码示例如下:
packagecom.ljq.test;
importorg.junit.Test;importorg.springframework.context.ApplicationContext;importorg.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 测试
*
* @author林计钦
* @version1.0 2013-11-4 下午10:56:04
*/publicclassHelloServiceTest {
@Test
publicvoidtestHelloWorld() {
// 1、读取配置文件实例化一个IOC容器
ApplicationContext context = newClassPathXmlApplicationContext("helloworld.xml");
// 2、从容器中获取Bean,注意此处完全“面向接口编程,而不是面向实现”
HelloService helloService = context.getBean("helloService", HelloService.class);
// 3、执行业务逻辑helloService.sayHello();
}
}
7、自此一个完整的Spring Hello World已完成,是不是很简单,让我们深入理解下容器和Bean吧。
2.2.4 详解IOC容器
在Spring IOC容器的代表就是org.springframework.beans包中的BeanFactory接口,BeanFactory接口提供了IOC容器最基本功能;而org.springframework.context包下的ApplicationContext接口扩展了BeanFactory,还提供了与Spring AOP集成、国际化处理、事件传播及提供不同层次的context实现 (如针对web应用的WebApplicationContext)。简单说, BeanFactory提供了IOC容器最基本功能,而 ApplicationContext 则增加了更多支持企业级功能支持。ApplicationContext完全继承BeanFactory,因而BeanFactory所具有的语义也适用于ApplicationContext。
容器实现一览:
• XmlBeanFactory:BeanFactory实现,提供基本的IOC容器功能,可以从classpath或文件系统等获取资源;
(1)File file = new File("fileSystemConfig.xml");
Resource resource = new FileSystemResource(file);
BeanFactory beanFactory = new XmlBeanFactory(resource);
(2)
Resource resource = new ClassPathResource("classpath.xml");
BeanFactory beanFactory = new XmlBeanFactory(resource);
• ClassPathXmlApplicationContext:ApplicationContext实现,从classpath获取配置文件;
BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath.xml");
• FileSystemXmlApplicationContext:ApplicationContext实现,从文件系统获取配置文件。
BeanFactory beanFactory = new FileSystemXmlApplicationContext("fileSystemConfig.xml");
ApplicationContext接口获取Bean方法简介:
• Object getBean(String name) 根据名称返回一个Bean,客户端需要自己进行类型转换;
• T getBean(String name, Class<T> requiredType) 根据名称和指定的类型返回一个Bean,客户端无需自己进行类型转换,如果类型转换失败,容器抛出异常;
• T getBean(Class<T> requiredType) 根据指定的类型返回一个Bean,客户端无需自己进行类型转换,如果没有或有多于一个Bean存在容器将抛出异常;
• Map<String, T> getBeansOfType(Class<T> type) 根据指定的类型返回一个键值为名字和值为Bean对象的Map,如果没有Bean对象存在则返回空的Map。
让我们来看下IOC容器到底是如何工作。在此我们以xml配置方式来分析一下:
一、准备配置文件:就像前边Hello World配置文件一样,在配置文件中声明Bean定义也就是为Bean配置元数据。
二、由IOC容器进行解析元数据: IOC容器的Bean Reader读取并解析配置文件,根据定义生成BeanDefinition配置元数据对象,IOC容器根据BeanDefinition进行实例化、配置及组装Bean。
三、实例化IOC容器:由客户端实例化容器,获取需要的Bean。
整个过程是不是很简单,执行过程如下,其实IOC容器很容易使用,主要是如何进行Bean定义。下一章我们详细介绍定义Bean。
2.2.5 小结
除了测试程序的代码外,也就是程序入口,所有代码都没有出现Spring任何组件,而且所有我们写的代码没有实现框架拥有的接口,因而能非常容易的替换掉Spring,是不是非入侵。
客户端代码完全面向接口编程,无需知道实现类,可以通过修改配置文件来更换接口实现,客户端代码不需要任何修改。是不是低耦合。
如果在开发初期没有真正的实现,我们可以模拟一个实现来测试,不耦合代码,是不是很方便测试。
Bean之间几乎没有依赖关系,是不是很容易重用。
反射实现 AOP 动态代理模式(Spring AOP 的实现 原理)
好长时间没有用过Spring了. 突然拿起书.我都发现自己对AOP都不熟悉了.
其实AOP的意思就是面向切面编程.
OO注重的是我们解决问题的方法(封装成Method),而AOP注重的是许多解决解决问题的方法中的共同点,是对OO思想的一种补充!
还是拿人家经常举的一个例子讲解一下吧:
比如说,我们现在要开发的一个应用里面有很多的业务方法,但是,我们现在要对这个方法的执行做全面监控,或部分监控.也许我们就会在要一些方法前去加上一条日志记录,
我们写个例子看看我们最简单的解决方案
我们先写一个接口IHello.java代码如下:
1package sinosoft.dj.aop.staticaop;
2
3public interface IHello {
4
8 void sayHello(String name);
9}
10
里面有个方法,用于输入"Hello" 加传进来的姓名;我们去写个类实现IHello接口
package sinosoft.dj.aop.staticaop;
public class Hello implements IHello {
public void sayHello(String name) {
System.out.println("Hello " + name);
}
}
现在我们要为这个业务方法加上日志记录的业务,我们在不改变原代码的情况下,我们会去怎么做呢?也许,你会去写一个类去实现IHello接口,并依赖Hello这个类.代码如下:
1package sinosoft.dj.aop.staticaop;
2
3public class HelloProxy implements IHello {
4 private IHello hello;
5
6 public HelloProxy(IHello hello) {
7 this.hello = hello;
8 }
9
10 public void sayHello(String name) {
11 Logger.logging(Level.DEBUGE, "sayHello method start.");
12 hello.sayHello(name);
13 Logger.logging(Level.INFO, "sayHello method end!");
14
15 }
16
17}
18
其中.Logger类和Level枚举代码如下:
Logger.java
1package sinosoft.dj.aop.staticaop;
2
3import java.util.Date;
4
5public class Logger{
6
11 public static void logging(Level level, String context) {
12 if (level.equals(Level.INFO)) {
13 System.out.println(new Date().toLocaleString() + " " + context);
14 }
15 if (level.equals(Level.DEBUGE)) {
16 System.err.println(new Date() + " " + context);
17 }
18 }
19
20}
21Level.java
1package sinosoft.dj.aop.staticaop;
2
3public enum Level {
4 INFO,DEBUGE;
5}
6那我们去写个测试类看看,代码如下:
Test.java
1package sinosoft.dj.aop.staticaop;
2
3public class Test {
4 public static void main(String[] args) {
5 IHello hello = new HelloProxy(new Hello());
6 hello.sayHello("Doublej");
7 }
8}
9运行以上代码我们可以得到下面结果:
Tue Mar 04 20:57:12 CST 2008 sayHello method start.
Hello Doublej
2008-3-4 20:57:12 sayHello method end!
从上面的代码我们可以看出,hello对象是被HelloProxy这个所谓的代理态所创建的.这样,如果我们以后要把日志记录的功能去掉.那我们只要把得到hello对象的代码改成以下:
1package sinosoft.dj.aop.staticaop;
2
3public class Test {
4 public static void main(String[] args) {
5 IHello hello = new Hello();
6 hello.sayHello("Doublej");
7 }
8}
9
上面代码,可以说是AOP最简单的实现!
但是我们会发现一个问题,如果我们像Hello这样的类很多,那么,我们是不是要去写很多个HelloProxy这样的类呢.没错,是的.其实也是一种很麻烦的事.在jdk1.3以后.jdk跟我们提供了一个API java.lang.reflect.InvocationHandler的类. 这个类可以让我们在JVM调用某个类的方法时动态的为些方法做些什么事.让我们把以上的代码改一下来看看效果.
同样,我们写一个IHello的接口和一个Hello的实现类.在接口中.我们定义两个方法;代码如下 :
IHello.java
1package sinosoft.dj.aop.proxyaop;
2
3public interface IHello {
4
8 void sayHello(String name);
9
13 void sayGoogBye(String name);
14}
15
Hello.java
1package sinosoft.dj.aop.proxyaop;
2
3public class Hello implements IHello {
4
5 public void sayHello(String name) {
6 System.out.println("Hello " + name);
7 }
8 public void sayGoogBye(String name) {
9 System.out.println(name+" GoodBye!");
10 }
11}
12
我们一样的去写一个代理类.只不过.让这个类去实现java.lang.reflect.InvocationHandler接口,代码如下:
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.InvocationHandler;
4import java.lang.reflect.Method;
5import java.lang.reflect.Proxy;
6
7public class DynaProxyHello implements InvocationHandler {
8
9
12 private Object delegate;
13
14
21 public Object bind(Object delegate) {
22 this.delegate = delegate;
23 return Proxy.newProxyInstance(
24 this.delegate.getClass().getClassLoader(), this.delegate
25 .getClass().getInterfaces(), this);
26 }
27
31 public Object invoke(Object proxy, Method method, Object[] args)
32 throws Throwable {
33 Object result = null;
34 try {
35 //执行原来的方法之前记录日志
36 Logger.logging(Level.DEBUGE, method.getName() + " Method end .");
37
38 //JVM通过这条语句执行原来的方法(反射机制)
39 result = method.invoke(this.delegate, args);
40 //执行原来的方法之后记录日志
41 Logger.logging(Level.INFO, method.getName() + " Method Start!");
42 } catch (Exception e) {
43 e.printStackTrace();
44 }
45 //返回方法返回值给调用者
46 return result;
47 }
48
49}
50
上面类中出现的Logger类和Level枚举还是和上一上例子的实现是一样的.这里就不贴出代码了.
让我们写一个Test类去测试一下.代码如下:
Test.java
1package sinosoft.dj.aop.proxyaop;
2
3public class Test {
4 public static void main(String[] args) {
5 IHello hello = (IHello)new DynaProxyHello().bind(new Hello());
6 hello.sayGoogBye("Double J");
7 hello.sayHello("Double J");
8
9 }
10}
11
运行输出的结果如下:
Tue Mar 04 21:24:03 CST 2008 sayGoogBye Method end .
Double J GoodBye!
2008-3-4 21:24:03 sayGoogBye Method Start!
Tue Mar 04 21:24:03 CST 2008 sayHello Method end .
Hello Double J
2008-3-4 21:24:03 sayHello Method Start!
由于线程的关系,第二个方法的开始出现在第一个方法的结束之前.这不是我们所关注的!
从上面的例子我们看出.只要你是采用面向接口编程,那么,你的任何对象的方法执行之前要加上记录日志的操作都是可以的.他(DynaPoxyHello)自动去代理执行被代理对象(Hello)中的每一个方法,一个java.lang.reflect.InvocationHandler接口就把我们的代理对象和被代理对象解藕了.但是,我们又发现还有一个问题,这个DynaPoxyHello对象只能跟我们去在方法前后加上日志记录的操作.我们能不能把DynaPoxyHello对象和日志操作对象(Logger)解藕呢?
结果是肯定的.让我们来分析一下我们的需求.
我们要在被代理对象的方法前面或者后面去加上日志操作代码(或者是其它操作的代码),
那么,我们可以抽象出一个接口,这个接口里就只有两个方法,一个是在被代理对象要执行方法之前执行的方法,我们取名为start,第二个方法就是在被代理对象执行方法之后执行的方法,我们取名为end .接口定义如下 :
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.Method;
4
5public interface IOperation {
6
10 void start(Method method);
11
15 void end(Method method);
16}
17
我们去写一个实现上面接口的类.我们把作他真正的操作者,如下面是日志操作者的一个类:
LoggerOperation.java
package sinosoft.dj.aop.proxyaop;
import java.lang.reflect.Method;
public class LoggerOperation implements IOperation {
public void end(Method method) {
Logger.logging(Level.DEBUGE, method.getName() + " Method end .");
}
public void start(Method method) {
Logger.logging(Level.INFO, method.getName() + " Method Start!");
}
}
然后我们要改一下代理对象DynaProxyHello中的代码.如下:
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.InvocationHandler;
4import java.lang.reflect.Method;
5import java.lang.reflect.Proxy;
6
7public class DynaProxyHello implements InvocationHandler {
8
11 private Object proxy;
12
15 private Object delegate;
16
17
24 public Object bind(Object delegate,Object proxy) {
25
26 this.proxy = proxy;
27 this.delegate = delegate;
28 return Proxy.newProxyInstance(
29 this.delegate.getClass().getClassLoader(), this.delegate
30 .getClass().getInterfaces(), this);
31 }
32
36 public Object invoke(Object proxy, Method method, Object[] args)
37 throws Throwable {
38 Object result = null;
39 try {
40 //反射得到操作者的实例
41 Class clazz = this.proxy.getClass();
42 //反射得到操作者的Start方法
43 Method start = clazz.getDeclaredMethod("start",
44 new Class[] { Method.class });
45 //反射执行start方法
46 start.invoke(this.proxy, new Object[] { method });
47 //执行要处理对象的原本方法
48 result = method.invoke(this.delegate, args);
49// 反射得到操作者的end方法
50 Method end = clazz.getDeclaredMethod("end",
51 new Class[] { Method.class });
52// 反射执行end方法
53 end.invoke(this.proxy, new Object[] { method });
54
55 } catch (Exception e) {
56 e.printStackTrace();
57 }
58 return result;
59 }
60
61}
62
然后我们把Test.java中的代码改一下.测试一下:
package sinosoft.dj.aop.proxyaop;
public class Test {
public static void main(String[] args) {
IHello hello = (IHello)new DynaProxyHello().bind(new Hello(),new LoggerOperation());
hello.sayGoogBye("Double J");
hello.sayHello("Double J");
}
}
结果还是一样的吧.
如果你想在每个方法之前加上日志记录,而不在方法后加上日志记录.你就把LoggerOperation类改成如下:
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.Method;
4
5public class LoggerOperation implements IOperation {
6
7 public void end(Method method) {
8 //Logger.logging(Level.DEBUGE, method.getName() + " Method end .");
9 }
10
11 public void start(Method method) {
12 Logger.logging(Level.INFO, method.getName() + " Method Start!");
13 }
14
15}
16
运行一下.你就会发现,每个方法之后没有记录日志了. 这样,我们就把代理者和操作者解藕了!
下面留一个问题给大家,如果我们不想让所有方法都被日志记录,我们应该怎么去解藕呢.?
我的想法是在代理对象的public Object invoke(Object proxy, Method method, Object[] args)方法里面加上个if(),对传进来的method的名字进行判断,判断的条件存在XML里面.这样我们就可以配置文件时行解藕了.如果有兴趣的朋友可以把操作者,被代理者,都通过配置文件进行配置 ,那么就可以写一个简单的SpringAOP框架了.
1.关于spring ioc
这段时间也着实好好的看了下spring的相关书籍,对其也有了大概和初步的认识和理解,虽然之前也一直听说spring是一个非常优秀的开源框架,可一直没有机会学习和使用(是不是有点落伍了?呵呵),所以呢,这段时间就重点学习了spring(一个星期的时间当然是入门级的啦~~)
大家一直都说spring的IOC如何如何的强大,其实我倒觉得不是IOC如何的强大,说白了IOC其实也非常的简单。我们先从IOC说起,这个概念其实是从我们平常new一个对象的对立面来说的,我们平常使用对象的时候,一般都是直接使用关键字类new一个对象,那这样有什么坏处呢?其实很显然的,使用new那么就表示当前模块已经不知不觉的和new的对象耦合了,而我们通常都是更高层次的抽象模块调用底层的实现模块,这样也就产生了模块依赖于具体的实现,这样与我们JAVA中提倡的面向接口面向抽象编程是相冲突的,而且这样做也带来系统的模块架构问题。很简单的例子,我们在进行数据库操作的时候,总是业务层调用DAO层,当然我们的DAO一般都是会采用接口开发,这在一定程度上满足了松耦合,使业务逻辑层不依赖于具体的数据库DAO层。但是我们在使用的时候还是会new一个特定数据库的DAO层,这无形中也与特定的数据库绑定了,虽然我们可以使用抽象工厂模式来获取DAO实现类,但除非我们一次性把所有数据库的DAO写出来,否则在进行数据库迁移的时候我们还是得修改DAO工厂类。
那我们使用IOC能达到什么呢?IOC,就是DAO接口的实现不再是业务逻辑层调用工厂类去获取,而是通过容器(比如spring)来自动的为我们的业务层设置DAO的实现类。这样整个过程就反过来,以前是我们业务层主动去获取DAO,而现在是DAO主动被设置到业务逻辑层中来了,这也就是反转控制的由来。通过IOC,我们就可以在不修改任何代码的情况下,无缝的实现数据库的换库迁移,当然前提还是必须得写一个实现特定数据库的DAO。我们把DAO普遍到更多的情况下,那么IOC就为我们带来更大的方便性,比如一个接口的多个实现,我们只需要配置一下就ok了,而不需要再一个个的写工厂来来获取了。这就是IOC为我们带来的模块的松耦合和应用的便利性。
那为什么说IOC很简单呢?说白了其实就是由我们平常的new转成了使用反射来获取类的实例,相信任何人只要会用java的反射机制,那么自己写一个IOC框架也不是不可能的。比如:
……
public ObjectgetInstance(String className) throws Exception
{
Object obj = Class.forName(className).newInstance();
Method[] methods = obj.getClass().getMethods();
for (Method method : methods) {
if (method.getName().intern() == "setString") {
method.invoke(obj, "hello world!");
}
}
}
……
上面的一个方法我们就很简单的使用了反射为指定的类的setString方法来设置一个hello world!字符串。其实可以看到IOC真的很简单,当然了IOC简单并不表示spring的IOC就简单,spring的IOC的功能强大就在于有一系列非常强大的配置文件维护类,它们可以维护spring配置文件中的各个类的关系,这才是spring的IOC真正强大的地方。在spring的Bean定义文件中,不仅可以为定义Bean设置属性,还支持Bean之间的继承、Bean的抽象和不同的获取方式等等功能。
下次俺再把spring的Bean配置的相关心得和大家一起分享下,如果说的不好,大家可以提意见哦,可千万不要仍臭鸡蛋,嘿嘿~~~~
2.关于spring aop
反射实现 AOP 动态代理模式(Spring AOP 的实现 原理)
好长时间没有用过Spring了. 突然拿起书.我都发现自己对AOP都不熟悉了.
其实AOP的意思就是面向切面编程.
OO注重的是我们解决问题的方法(封装成Method),而AOP注重的是许多解决解决问题的方法中的共同点,是对OO思想的一种补充!
还是拿人家经常举的一个例子讲解一下吧:
比如说,我们现在要开发的一个应用里面有很多的业务方法,但是,我们现在要对这个方法的执行做全面监控,或部分监控.也许我们就会在要一些方法前去加上一条日志记录,
我们写个例子看看我们最简单的解决方案
我们先写一个接口IHello.java代码如下:
1package sinosoft.dj.aop.staticaop;
2
3public interface IHello {
4 /** *//**
5 * 假设这是一个业务方法
6 * @param name
7 */
8 void sayHello(String name);
9}
10
里面有个方法,用于输入"Hello" 加传进来的姓名;我们去写个类实现IHello接口
package sinosoft.dj.aop.staticaop;
public class Hello implements IHello {
public void sayHello(String name) {
System.out.println("Hello " + name);
}
}
现在我们要为这个业务方法加上日志记录的业务,我们在不改变原代码的情况下,我们会去怎么做呢?也许,你会去写一个类去实现IHello接口,并依赖Hello这个类.代码如下:
1package sinosoft.dj.aop.staticaop;
2
3public class HelloProxy implements IHello {
4 private IHello hello;
5
6 public HelloProxy(IHello hello) {
7 this.hello = hello;
8 }
9
10 public void sayHello(String name) {
11 Logger.logging(Level.DEBUGE, "sayHello method start.");
12 hello.sayHello(name);
13 Logger.logging(Level.INFO, "sayHello method end!");
14
15 }
16
17}
18
其中.Logger类和Level枚举代码如下:
Logger.java
1package sinosoft.dj.aop.staticaop;
2
3import java.util.Date;
4
5public class Logger{
6 /** *//**
7 * 根据等级记录日志
8 * @param level
9 * @param context
10 */
11 public static void logging(Level level, String context) {
12 if (level.equals(Level.INFO)) {
13 System.out.println(new Date().toLocaleString() + " " + context);
14 }
15 if (level.equals(Level.DEBUGE)) {
16 System.err.println(new Date() + " " + context);
17 }
18 }
19
20}
21Level.java
1package sinosoft.dj.aop.staticaop;
2
3public enum Level {
4 INFO,DEBUGE;
5}
6那我们去写个测试类看看,代码如下:
Test.java
1package sinosoft.dj.aop.staticaop;
2
3public class Test {
4 public static void main(String[] args) {
5 IHello hello = new HelloProxy(new Hello());
6 hello.sayHello("Doublej");
7 }
8}
9运行以上代码我们可以得到下面结果:
Tue Mar 04 20:57:12 CST 2008 sayHello method start.
Hello Doublej
2008-3-4 20:57:12 sayHello method end!
从上面的代码我们可以看出,hello对象是被HelloProxy这个所谓的代理态所创建的.这样,如果我们以后要把日志记录的功能去掉.那我们只要把得到hello对象的代码改成以下:
1package sinosoft.dj.aop.staticaop;
2
3public class Test {
4 public static void main(String[] args) {
5 IHello hello = new Hello();
6 hello.sayHello("Doublej");
7 }
8}
9
上面代码,可以说是AOP最简单的实现!
但是我们会发现一个问题,如果我们像Hello这样的类很多,那么,我们是不是要去写很多个HelloProxy这样的类呢.没错,是的.其实也是一种很麻烦的事.在jdk1.3以后.jdk跟我们提供了一个API java.lang.reflect.InvocationHandler的类. 这个类可以让我们在JVM调用某个类的方法时动态的为些方法做些什么事.让我们把以上的代码改一下来看看效果.
同样,我们写一个IHello的接口和一个Hello的实现类.在接口中.我们定义两个方法;代码如下 :
IHello.java
1package sinosoft.dj.aop.proxyaop;
2
3public interface IHello {
4 /** *//**
5 * 业务处理A方法
6 * @param name
7 */
8 void sayHello(String name);
9 /** *//**
10 * 业务处理B方法
11 * @param name
12 */
13 void sayGoogBye(String name);
14}
15
Hello.java
1package sinosoft.dj.aop.proxyaop;
2
3public class Hello implements IHello {
4
5 public void sayHello(String name) {
6 System.out.println("Hello " + name);
7 }
8 public void sayGoogBye(String name) {
9 System.out.println(name+" GoodBye!");
10 }
11}
12
我们一样的去写一个代理类.只不过.让这个类去实现java.lang.reflect.InvocationHandler接口,代码如下:
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.InvocationHandler;
4import java.lang.reflect.Method;
5import java.lang.reflect.Proxy;
6
7public class DynaProxyHello implements InvocationHandler {
8
9 /** *//**
10 * 要处理的对象(也就是我们要在方法的前后加上业务逻辑的对象,如例子中的Hello)
11 */
12 private Object delegate;
13
14 /** *//**
15 * 动态生成方法被处理过后的对象 (写法固定)
16 *
17 * @param delegate
18 * @param proxy
19 * @return
20 */
21 public Object bind(Object delegate) {
22 this.delegate = delegate;
23 return Proxy.newProxyInstance(
24 this.delegate.getClass().getClassLoader(), this.delegate
25 .getClass().getInterfaces(), this);
26 }
27 /** *//**
28 * 要处理的对象中的每个方法会被此方法送去JVM调用,也就是说,要处理的对象的方法只能通过此方法调用
29 * 此方法是动态的,不是手动调用的
30 */
31 public Object invoke(Object proxy, Method method, Object[] args)
32 throws Throwable {
33 Object result = null;
34 try {
35 //执行原来的方法之前记录日志
36 Logger.logging(Level.DEBUGE, method.getName() + " Method end .");
37
38 //JVM通过这条语句执行原来的方法(反射机制)
39 result = method.invoke(this.delegate, args);
40 //执行原来的方法之后记录日志
41 Logger.logging(Level.INFO, method.getName() + " Method Start!");
42 } catch (Exception e) {
43 e.printStackTrace();
44 }
45 //返回方法返回值给调用者
46 return result;
47 }
48
49}
50
上面类中出现的Logger类和Level枚举还是和上一上例子的实现是一样的.这里就不贴出代码了.
让我们写一个Test类去测试一下.代码如下:
Test.java
1package sinosoft.dj.aop.proxyaop;
2
3public class Test {
4 public static void main(String[] args) {
5 IHello hello = (IHello)new DynaProxyHello().bind(new Hello());
6 hello.sayGoogBye("Double J");
7 hello.sayHello("Double J");
8
9 }
10}
11
运行输出的结果如下:
Tue Mar 04 21:24:03 CST 2008 sayGoogBye Method end .
Double J GoodBye!
2008-3-4 21:24:03 sayGoogBye Method Start!
Tue Mar 04 21:24:03 CST 2008 sayHello Method end .
Hello Double J
2008-3-4 21:24:03 sayHello Method Start!
由于线程的关系,第二个方法的开始出现在第一个方法的结束之前.这不是我们所关注的!
从上面的例子我们看出.只要你是采用面向接口编程,那么,你的任何对象的方法执行之前要加上记录日志的操作都是可以的.他(DynaPoxyHello)自动去代理执行被代理对象(Hello)中的每一个方法,一个java.lang.reflect.InvocationHandler接口就把我们的代理对象和被代理对象解藕了.但是,我们又发现还有一个问题,这个DynaPoxyHello对象只能跟我们去在方法前后加上日志记录的操作.我们能不能把DynaPoxyHello对象和日志操作对象(Logger)解藕呢?
结果是肯定的.让我们来分析一下我们的需求.
我们要在被代理对象的方法前面或者后面去加上日志操作代码(或者是其它操作的代码),
那么,我们可以抽象出一个接口,这个接口里就只有两个方法,一个是在被代理对象要执行方法之前执行的方法,我们取名为start,第二个方法就是在被代理对象执行方法之后执行的方法,我们取名为end .接口定义如下 :
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.Method;
4
5public interface IOperation {
6 /** *//**
7 * 方法执行之前的操作
8 * @param method
9 */
10 void start(Method method);
11 /** *//**
12 * 方法执行之后的操作
13 * @param method
14 */
15 void end(Method method);
16}
17
我们去写一个实现上面接口的类.我们把作他真正的操作者,如下面是日志操作者的一个类:
LoggerOperation.java
package sinosoft.dj.aop.proxyaop;
import java.lang.reflect.Method;
public class LoggerOperation implements IOperation {
public void end(Method method) {
Logger.logging(Level.DEBUGE, method.getName() + " Method end .");
}
public void start(Method method) {
Logger.logging(Level.INFO, method.getName() + " Method Start!");
}
}
然后我们要改一下代理对象DynaProxyHello中的代码.如下:
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.InvocationHandler;
4import java.lang.reflect.Method;
5import java.lang.reflect.Proxy;
6
7public class DynaProxyHello implements InvocationHandler {
8 /** *//**
9 * 操作者
10 */
11 private Object proxy;
12 /** *//**
13 * 要处理的对象(也就是我们要在方法的前后加上业务逻辑的对象,如例子中的Hello)
14 */
15 private Object delegate;
16
17 /** *//**
18 * 动态生成方法被处理过后的对象 (写法固定)
19 *
20 * @param delegate
21 * @param proxy
22 * @return
23 */
24 public Object bind(Object delegate,Object proxy) {
25
26 this.proxy = proxy;
27 this.delegate = delegate;
28 return Proxy.newProxyInstance(
29 this.delegate.getClass().getClassLoader(), this.delegate
30 .getClass().getInterfaces(), this);
31 }
32 /** *//**
33 * 要处理的对象中的每个方法会被此方法送去JVM调用,也就是说,要处理的对象的方法只能通过此方法调用
34 * 此方法是动态的,不是手动调用的
35 */
36 public Object invoke(Object proxy, Method method, Object[] args)
37 throws Throwable {
38 Object result = null;
39 try {
40 //反射得到操作者的实例
41 Class clazz = this.proxy.getClass();
42 //反射得到操作者的Start方法
43 Method start = clazz.getDeclaredMethod("start",
44 new Class[] { Method.class });
45 //反射执行start方法
46 start.invoke(this.proxy, new Object[] { method });
47 //执行要处理对象的原本方法
48 result = method.invoke(this.delegate, args);
49// 反射得到操作者的end方法
50 Method end = clazz.getDeclaredMethod("end",
51 new Class[] { Method.class });
52// 反射执行end方法
53 end.invoke(this.proxy, new Object[] { method });
54
55 } catch (Exception e) {
56 e.printStackTrace();
57 }
58 return result;
59 }
60
61}
62
然后我们把Test.java中的代码改一下.测试一下:
package sinosoft.dj.aop.proxyaop;
public class Test {
public static void main(String[] args) {
IHello hello = (IHello)new DynaProxyHello().bind(new Hello(),new LoggerOperation());
hello.sayGoogBye("Double J");
hello.sayHello("Double J");
}
}
结果还是一样的吧.
如果你想在每个方法之前加上日志记录,而不在方法后加上日志记录.你就把LoggerOperation类改成如下:
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.Method;
4
5public class LoggerOperation implements IOperation {
6
7 public void end(Method method) {
8 //Logger.logging(Level.DEBUGE, method.getName() + " Method end .");
9 }
10
11 public void start(Method method) {
12 Logger.logging(Level.INFO, method.getName() + " Method Start!");
13 }
14
15}
16
相关推荐
java基础知识点总结及面试问题java基础知识点总结及面试问题java基础知识点总结及面试java基础知识点总结及面试问题
标题:“2021 - JAVA秋招基础知识点面试题”的知识点总结 1. JDK与JRE的区别: JDK是Java开发工具包,它包含了JRE和一些其他工具,如编译器javac和调试工具等。JRE是Java运行环境,仅用于运行Java程序。简单来说,...
java面试基础知识点总结
Java面试笔记 225道Java面试题JAVA面试基础知识点总结Java数据结构题 JAVA笔试面试WORD资料汇总(19个): 2014年最新Java笔试题及答案.docx 225道Java面试题 学会了Java面试随你问.docx Ant和Maven的作用是什么?两者...
Java基础知识:包括Java语言特性、面向对象编程、集合框架、异常处理等基础知识点。 数据库和SQL:涵盖数据库基础知识、SQL语句的编写和优化、数据库事务等相关内容。 Web开发:包括常用的Web开发框架(如Spring、...
1. **Java基础**:面试中,面试官通常会从基本语法开始,包括数据类型、变量、运算符、流程控制(if、switch、for、while)、类与对象、封装、继承、多态等。理解并能灵活运用这些基础知识是成为一名合格Java开发者...
这是一套Java核心技术基础使用手册,包含Java 基础核心总结、Java核心基础、Java核心知识、Java 基础面试题总结等,内含最强 Java 核心知识点整理及思维导图,需要的朋友可下载试试! Java是一门编程语言,Java发展...
总结整理的Android面试Java基础知识点面试资料精编汇总文档资料合集: Android面试 常见58题.docx Android常见原理性面试专题.docx Android面试常问基础知识点.docx BAT Android面试20题详解.docx Java基础面试题....
为了在激烈的Java开发者竞争中脱颖而出,深入理解Java基础知识、JVM(Java虚拟机)、线程并发以及常用框架是至关重要的。这份"java-java面试题库整理-基础-JVM-线程并发-框架等.zip"文件提供了一个全面的复习资源,...
介绍Java求职面试过程过程中的相关知识点,分为java基础,web,框架等基础知识
最近的java 面试知识点, 比较全的java基础知识面试知识,linux
这份"【Java面试资料】-JAVA核心面试知识点整理"PDF文档,很可能是求职者或开发者提升技能、准备面试的重要参考资料。以下是根据标题和描述提炼出的一些关键Java面试知识点,以及它们在实际编程和面试中的重要性。 ...
Java基础知识面试题 Java是一种广泛使用的编程语言,具有平台独立性、面向对象、简单性和安全性等特点。Java基础知识是每个Java开发人员必须掌握的基本技能。以下是Java基础知识面试题的相关知识点: 1. Java语言...
Java面试是评估程序员技能的重要环节,对于求职者来...以上仅是对可能包含知识点的概述,具体的“Java面试题大全--new”资源中会有更深入的讲解和实例分析,帮助求职者巩固和提升Java编程技能,从而在面试中表现出色。
Java基础是编程学习的核心部分,本总结主要涵盖了Java语言的基础概念和常见问题,适用于初学者和需要回顾基础知识的开发者。以下是对这些知识点的详细解释: 1. **基本数据类型**: - Java提供了八种基本数据类型...
《Java面试宝典2018-最全面试...面试宝典2018为求职者提供了一个全方位、多角度的学习和复习平台,从基础知识点到企业实战经验,帮助求职者全面提高自己的技术实力和面试技巧,是当时极为受欢迎的一本面试参考资料。
1. **基础语法与数据类型**:面试中,Java的基础知识是必不可少的,包括变量、常量、数据类型(如基本类型和引用类型)、运算符、流程控制语句(如if、for、while)等。深入理解这些概念,有助于处理各种编程问题。 ...