java什么是交叉引用,Java 多线程 资源冲突
大家好,感谢邀请,今天来为大家分享一下java什么是交叉引用的问题,以及和Java 多线程 资源冲突的一些困惑,大家要是还不太明白的话,也没有关系,因为接下来将为大家分享,希望可以帮助到大家,解决大家的问题,下面就开始吧!
java方法问题
异常的类别异常的分类有不同方式。这里,我们将讨论从 EJB的角度如何对异常进行分类。EJB规范将异常大致分成三类:JVM异常:这种类型的异常由 JVM抛出。OutOfMemoryError就是 JVM异常的一个常见示例。对 JVM异常您无能为力。它们表明一种致命的情况。唯一得体的退出办法是停止应用程序服务器(可能要增加硬件资源),然后重新启动系统。应用程序异常:应用程序异常是一种定制异常,由应用程序或第三方的库抛出。这些本质上是受查异常(checked exception);它们预示了业务逻辑中的某个条件尚未满足。在这样的情况下,EJB方法的调用者可以得体地处理这种局面并采用另一条备用途径。系统异常:在大多数情况下,系统异常由 JVM作为 RuntimeException的子类抛出。例如,NullPointerException或 ArrayOutOfBoundsException将因代码中的错误而被抛出。另一种类型的系统异常在系统碰到配置不当的资源(例如,拼写错误的 JNDI查找(JNDI lookup))时发生。在这种情况下,系统就将抛出一个受查异常。捕获这些受查系统异常并将它们作为非受查异常(unchecked exception)抛出颇有意义。最重要的规则是,如果您对某个异常无能为力,那么它就是一个系统异常并且应当作为非受查异常抛出。受查异常是一个作为 java.lang.Exception的子类的 Java类。通过从 java.lang.Exception派生子类,就强制您在编译时捕获这个异常。相反地,非受查异常则是一个作为 java.lang.RuntimeException的子类的 Java类。从 java.lang.RuntimeException派生子类确保了编译器不会强制您捕获这个异常。清单 1.三种常见的异常处理做法100 try{101 OrderHome homeObj= EJBHomeFactory.getInstance().getOrderHome();102 Collection orderCollection= homeObj.findByCustomerId(id);103 iterator orderItter= orderCollection.iterator();104 while(orderIter.hasNext()){105 Order orderRemote=(OrderRemote) orderIter.getNext();106 OrderValue orderVal= orderRemote.getValue();107 if(orderVal.getDate()<"mm/dd/yyyy"){108 OrderItemHome itemHome=EJBHomeFactory.getInstance().getItemHome();109 Collection itemCol= itemHome.findByOrderId(orderId)110 Iterator itemIter= itemCol.iterator();111 while(itemIter.hasNext()){112 OrderItem item=(OrderItem) itemIter.getNext();113 item.remove();114}115 orderRemote.remove();116}117}118} catch(NamingException ne){119 throw new EJBException("Naming Exception occurred");120} catch(FinderException fe){121 fe.printStackTrace();122 throw new EJBException("Finder Exception occurred");123} catch(RemoteException re){124 re.printStackTrace();125//Some code to log the message126 throw new EJBException(re);127}EJB异常处理探试法EJB组件应抛出哪些异常?您应将它们记录到系统中的什么地方?这两个问题盘根错结、相互联系,应该一起解决。解决办法取决于以下因素:您的 EJB系统设计:在良好的 EJB设计中,客户机绝不调用实体 EJB组件上的方法。多数实体 EJB方法调用发生在会话 EJB组件中。如果您的设计遵循这些准则,则您应该用会话 EJB组件来记录异常。如果客户机直接调用了实体 EJB方法,则您还应该把消息记录到实体 EJB组件中。然而,存在一个难题:相同的实体 EJB方法可能也会被会话 EJB组件调用。在这种情形下,如何避免重复记录呢?类似地,当一个会话 EJB组件调用其它实体 EJB方法时,您如何避免重复记录呢?很快我们就将探讨一种处理这两种情况的通用解决方案。(请注意,EJB 1.1并未从体系结构上阻止客户机调用实体 EJB组件上的方法。在 EJB 2.0中,您可以通过为实体 EJB组件定义本地接口规定这种限制。处理应用程序异常在这一部分及其后的几个部分中,我们将更仔细地研究用 EJB异常处理应用程序异常和系统异常,以及 Web层设计。作为这个讨论的一部分,我们将探讨处理从会话和实体 EJB组件抛出的异常的不同方式。实体 EJB组件中的应用程序异常清单 2显示了实体 EJB的一个 ejbCreate()方法。这个方法的调用者传入一个 OrderItemValue并请求创建一个 OrderItem实体。因为 OrderItemValue没有名称,所以抛出了 CreateException。清单 2显示了 CreateException的一个很典型的用法。类似地,如果方法的输入参数的值不正确,则查找程序方法将抛出 FinderException。然而,如果您在使用容器管理的持久性(CMP),则开发者无法控制查找程序方法,从而 FinderException永远不会被 CMP实现抛出。尽管如此,在 Home接口的查找程序方法的 throws子句中声明 FinderException还是要更好一些。RemoveException是另一个应用程序异常,它在实体被删除时被抛出。从实体 EJB组件抛出的应用程序异常基本上限定为这三种类型(CreateException、FinderException和 RemoveException)及它们的子类。多数应用程序异常都来源于会话 EJB组件,因为那里是作出智能决策的地方。实体 EJB组件一般是哑类,它们的唯一职责就是创建和取回数据。会话 EJB组件中的应用程序异常清单 3显示了来自会话 EJB组件的一个方法。这个方法的调用者设法订购 n件某特定类型的某商品。SessionEJB()方法计算出仓库中的数量不够,于是抛出 NotEnoughStockException。NotEnoughStockException适用于特定于业务的场合;当抛出了这个异常时,调用者会得到采用另一个备用途径的建议,让他订购更少数量的商品。清单 3.会话 EJB组件中的样本容器回调方法public ItemValueObject[] placeOrder(int n, ItemType itemType) throwsNotEnoughStockException{//Check Inventory.Collection orders= ItemHome.findByItemType(itemType);if(orders.size()< n){throw NotEnoughStockException("Insufficient stock for"+ itemType);}}
处理系统异常
系统异常处理是比应用程序异常处理更为复杂的论题。由于会话 EJB组件和实体 EJB组件处理系统异常的方式相似,所以,对于本部分的所有示例,我们都将着重于实体 EJB组件,不过请记住,其中的大部分示例也适用于处理会话 EJB组件。当引用其它 EJB远程接口时,实体 EJB组件会碰到 RemoteException,而查找其它 EJB组件时,则会碰到 NamingException,如果使用 bean管理的持久性(BMP),则会碰到 SQLException。与这些类似的受查系统异常应该被捕获并作为 EJBException或它的一个子类抛出。原始的异常应被包装起来。清单 4显示了一种处理系统异常的办法,这种办法与处理系统异常的 EJB容器的行为一致。通过包装原始的异常并在实体 EJB组件中将它重新抛出,您就确保了能够在想记录它的时候访问该异常。清单 4.处理系统异常的一种常见方式try{OrderHome orderHome= EJBHomeFactory.getInstance().getOrderHome();Order order= orderHome.findByPrimaryKey(Integer id);} catch(NamingException ne){throw new EJBException(ne);} catch(SQLException se){throw new EJBException(se);} catch(RemoteException re){throw new EJBException(re);}避免重复记录通常,异常记录发生在会话 EJB组件中。但如果直接从 EJB层外部访问实体 EJB组件,又会怎么样呢?要是这样,您就不得不在实体 EJB组件中记录异常并抛出它。这里的问题是,调用者没办法知道异常是否已经被记录,因而很可能再次记录它,从而导致重复记录。更重要的是,调用者没办法访问初始记录时所生成的唯一的标识。任何没有交叉引用机制的记录都是毫无用处的。请考虑这种最糟糕的情形:单机 Java应用程序访问了实体 EJB组件中的一个方法 foo()。在一个名为 bar()的会话 EJB方法中也访问了同一个方法。一个 Web层客户机调用会话 EJB组件的方法 bar()并也记录了该异常。如果当从 Web层调用会话 EJB方法 bar()时在实体 EJB方法 foo()中发生了一个异常,则该异常将被记录到三个地方:先是在实体 EJB组件,然后是在会话 EJB组件,最后是在 Web层。而且,没有一个堆栈跟踪可以被交叉引用!幸运的是,解决这些问题用常规办法就可以很容易地做到。您所需要的只是一种机制,使调用者能够:访问唯一的标识查明异常是否已经被记录了您可以派生 EJBException的子类来存储这样的信息。清单 5显示了 LoggableEJBException子类:清单 5. LoggableEJBException? EJBException的一个子类public class LoggableEJBException extends EJBException{protected boolean isLogged;protected String uniqueID;public LoggableEJBException(Exception exc){super(exc);isLogged= false;uniqueID= ExceptionIDGenerator.getExceptionID();}....}类 LoggableEJBException有一个指示符标志(isLogged),用于检查异常是否已经被记录了。每当捕获一个 LoggableEJBException时,看一下该异常是否已经被记录了(isLogged== false)。如果 isLogged为 false,则记录该异常并把标志设置为 true。ExceptionIDGenerator类用当前时间和机器的主机名为异常生成唯一的标识。如果您喜欢,也可以用有想象力的算法来生成这个唯一的标识。如果您在实体 EJB组件中记录了异常,则这个异常将不会在别的地方被记录。如果您没有记录就在实体 EJB组件中抛出了 LoggableEJBException,则这个异常将被记录到会话 EJB组件中,但不记录到 Web层中。清单 6显示了使用这一技术重写后的清单 4。您还可以继承 LoggableException以适合于您的需要(通过给异常指定错误代码等)。清单 6.使用 LoggableEJBException的异常处理try{OrderHome orderHome= EJBHomeFactory.getInstance().getOrderHome();Order order= orderHome.findByPrimaryKey(Integer id);} catch(NamingException ne){throw new LoggableEJBException(ne);} catch(SQLException se){throw new LoggableEJBException(se);} catch(RemoteException re){Throwable t= re.detail;if(t!= null&& t instanceof Exception){throw new LoggableEJBException((Exception) re.detail);} else{throw new LoggableEJBException(re);}}记录 RemoteException从清单 6中,您可以看到 naming和 SQL异常在被抛出前被包装到了 LoggableEJBException中。但 RemoteException是以一种稍有不同?而且要稍微花点气力?的方式处理的。会话 EJB组件中的系统异常如果您决定记录会话 EJB异常,请使用清单 7所示的记录代码;否则,请抛出异常,如清单 6所示。您应该注意到,会话 EJB组件处理异常可有一种与实体 EJB组件不同的方式:因为大多数 EJB系统都只能从 Web层访问,而且会话 EJB可以作为 EJB层的虚包,所以,把会话 EJB异常的记录推迟到 Web层实际上是有可能做到的。它之所以不同,是因为在 RemoteException中,实际的异常将被存储到一个称为 detail(它是 Throwable类型的)的公共属性中。在大多数情况下,这个公共属性保存有一个异常。如果您调用 RemoteException的 printStackTrace,则除打印 detail的堆栈跟踪之外,它还会打印异常本身的堆栈跟踪。您不需要像这样的 RemoteException的堆栈跟踪。为了把您的应用程序代码从错综复杂的代码(例如 RemoteException的代码)中分离出来,这些行被重新构造成一个称为 ExceptionLogUtil的类。有了这个类,您所要做的只是每当需要创建 LoggableEJBException时调用 ExceptionLogUtil.createLoggableEJBException(e)。请注意,在清单 6中,实体 EJB组件并没有记录异常;不过,即便您决定在实体 EJB组件中记录异常,这个解决方案仍然行得通。清单 7显示了实体 EJB组件中的异常记录:清单 7.实体 EJB组件中的异常记录try{OrderHome orderHome= EJBHomeFactory.getInstance().getOrderHome();Order order= orderHome.findByPrimaryKey(Integer id);} catch(RemoteException re){LoggableEJBException le=ExceptionLogUtil.createLoggableEJBException(re);String traceStr= StackTraceUtil.getStackTrace(le);Category.getInstance(getClass().getName()).error(le.getUniqueID()+":"+ traceStr);le.setLogged(true);throw le;}您在清单 7中看到的是一个非常简单明了的异常记录机制。一旦捕获受查系统异常就创建一个新的 LoggableEJBException。接着,使用类 StackTraceUtil获取 LoggableEJBException的堆栈跟踪,把它作为一个字符串。然后,使用 Log4J category把该字符串作为一个错误加以记录。
StackTraceUtil类的工作原理
在清单 7中,您看到了一个新的称为 StackTraceUtil的类。因为 Log4J只能记录 String消息,所以这个类负责解决把堆栈跟踪转换成 String的问题。清单 8说明了 StackTraceUtil类的工作原理:清单 8. StackTraceUtil类public class StackTraceUtil{public static String getStackTrace(Exception e){StringWriter sw= new StringWriter();PrintWriter pw= new PrintWriter(sw);return sw.toString();}....}java.lang.Throwable中缺省的 printStackTrace()方法把出错消息记录到 System.err。Throwable还有一个重载的 printStackTrace()方法,它把出错消息记录到 PrintWriter或 PrintStream。上面的 StackTraceUtil中的方法把 StringWriter包装到 PrintWriter中。当 PrintWriter包含有堆栈跟踪时,它只是调用 StringWriter的 toString(),以获取该堆栈跟踪的 String表示。Web层的 EJB异常处理在 Web层设计中,把异常记录机制放到客户机端往往更容易也更高效。要能做到这一点,Web层就必须是 EJB层的唯一客户机。此外,Web层必须建立在以下模式或框架之一的基础上:模式:业务委派(Business Delegate)、FrontController或拦截过滤器(Intercepting Filter)框架:Struts或任何包含层次结构的类似于 MVC框架的框架为什么异常记录应该在客户机端上发生呢?嗯,首先,控制尚未传到应用程序服务器之外。所谓的客户机层在 J2EE应用程序服务器本身上运行,它由 JSP页、servlet或它们的助手类组成。其次,在设计良好的 Web层中的类有一个层次结构(例如:在业务委派(Business Delegate)类、拦截过滤器(Intercepting Filter)类、http请求处理程序(http request handler)类和 JSP基类(JSP base class)中,或者在 Struts Action类中),或者 FrontController servlet形式的单点调用。这些层次结构的基类或者 Controller类中的中央点可能包含有异常记录代码。对于基于会话 EJB记录的情况,EJB组件中的每一个方法都必须具有记录代码。随着业务逻辑的增加,会话 EJB方法的数量也会增加,记录代码的数量也会增加。Web层系统将需要更少的记录代码。如果您的 Web层和 EJB层在同一地方并且不需要支持任何其它类型的客户机,那么您应该考虑这一备用方案。不管怎样,记录机制不会改变;您可以使用与前面的部分所描述的相同技术。真实世界的复杂性到现在为止,您已经看到了简单情形的会话和实体 EJB组件的异常处理技术。然而,应用程序异常的某些组合可能会更令人费解,并且有多种解释。清单 9显示了一个示例。OrderEJB的 ejbCreate()方法试图获取 CustomerEJB的一个远程引用,这会导致 FinderException。OrderEJB和 CustomerEJB都是实体 EJB组件。您应该如何解释 ejbCreate()中的这个 FinderException呢?是把它当作应用程序异常对待呢(因为 EJB规范把它定义为标准应用程序异常),还是当作系统异常对待?清单 9. ejbCreate()方法中的 FinderExceptionpublic Object ejbCreate(OrderValue val) throws CreateException{try{if(value.getItemName()== null){throw new CreateException("Cannot create Order without a name");}String custId= val.getCustomerId();Customer cust= customerHome.fingByPrimaryKey(custId);this.customer= cust;} catch(FinderException ne){//How do you handle this Exception?} catch(RemoteException re){//This is clearly a System Exceptionthrow ExceptionLogUtil.createLoggableEJBException(re);}return null;}文字太多打不下了,具体你在看看类似的教程
Java里什么是引用类型
最简答来说除了8中基本类型以外剩下的都是引用类型
Java提供两种不同的类型:引用类型和原始类型(或内置类型)。Int是java的原始数据类型,Integer是java为int提供的封装类。Java为每个原始类型提供了封装类。
原始类型封装类
boolean-->Boolean
char--->Character
byte-->Byte
short-->Short
int-->Integer
long-->Long
float-->Float
double-->Double
引用类型和原始类型的行为完全不同,并且它们具有不同的语义。引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值。对象引用实例变量的缺省值为 null,而原始类型实例变量的缺省值与它们的类型有关。同时为了面向对象操作的一致性,这些基本类型都有相应的封装类型:Integer、Short、Long、Byte、Float、Double、Character等。
因为封装类型是对象,所以可以进行相应的很多对象能力函数操作,这样就可以提供很多基本类型难以完成的工作的完成和实现。
你可以通过以下方式来声明该类型。
int a,a为int类型的变量
char a,a为char类型的
String对象
1.首先String不属于8种基本数据类型,String是一个对象。
因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。
2. new String()和new String(“”)都是申明一个新的空字符串,是空串不是null;
3. String str=”punkll”;
String str=new String(“punkll”);的区别:
在这里,我们不谈堆,也不谈栈,只先简单引入常量池这个简单的概念。
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。
看例1:
String s0=”punkll”;
String s1=”punkll”;
String s2=”punk”+“ll”;
System.out.println( s0==s1);
System.out.println( s0==s2);
结果为:
true
true
首先,我们要知结果为道Java会确保一个字符串常量只有一个拷贝。
因为例子中的s0和s1中的”punkll”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”punk”和”ll”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中”punkll”的一个引用。
所以我们得出s0==s1==s2;
用new String()创建的字符串不是常量,不能在编译期就确定,所以new String()创建的字符串不放入常量池中,它们有自己的地址空间。
看例2:
String s0=”punkll”;
String s1=new String(”punkll”);
String s2=”punk”+ new String(“ll”);
System.out.println( s0==s1);
System.out.println( s0==s2);
System.out.println( s1==s2);
结果为:
false
false
false
例2中s0还是常量池中”punkll”的应用,s1因为无法在编译期确定,所以是运行时创建的新对象”punkll”的引用,s2因为有后半部分new String(“ll”)所以也无法在编译期确定,所以也是一个新创建对象”punkll”的应用;明白了这些也就知道为何得出此结果了。
Java 多线程 资源冲突
这是javaeye上非常经典的关于线程的帖子,写的非常通俗易懂的,适合任何读计算机的同学.
线程同步
我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括多个独立运行的线程(Thread)。
线程(Thread)是一份独立运行的程序,有自己专用的运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为什么要用这个很容易引起误解的词。既然大家都这么用,咱们也就只好这么将就。
线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
因此,关于线程同步,需要牢牢记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。
关于线程同步,需要牢牢记住的第二点是“共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。
关于线程同步,需要牢牢记住的第三点是,只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
关于线程同步,需要牢牢记住的第四点是:多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。
为了加深理解,下面举几个例子。
有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤:
(1)到市场上去,寻找并购买有潜力的样品。
(2)回到公司,写报告。
这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。
这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。
下面给这两个采购员增加一个工作步骤。采购员需要根据公司的“布告栏”上面公布的信息,安排自己的工作计划。
这两个采购员有可能同时走到布告栏的前面,同时观看布告栏上的信息。这一点问题都没有。因为布告栏是只读的,这两个采购员谁都不会去修改布告栏上写的信息。
下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。
如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。
如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。
上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步。
同步锁
前面讲了为什么要线程同步,下面我们就来看如何才能线程同步。
线程同步的基本实现思路还是比较容易理解的。我们可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。
生活中,我们也可能会遇到这样的例子。一些超市的外面提供了一些自动储物箱。每个储物箱都有一把锁,一把钥匙。人们可以使用那些带有钥匙的储物箱,把东西放到储物箱里面,把储物箱锁上,然后把钥匙拿走。这样,该储物箱就被锁住了,其他人不能再访问这个储物箱。(当然,真实的储物箱钥匙是可以被人拿走复制的,所以不要把贵重物品放在超市的储物箱里面。于是很多超市都采用了电子密码锁。)
线程同步锁这个模型看起来很直观。但是,还有一个严峻的问题没有解决,这个同步锁应该加在哪里?
当然是加在共享资源上了。反应快的读者一定会抢先回答。
没错,如果可能,我们当然尽量把同步锁加在共享资源上。一些比较完善的共享资源,比如,文件系统,数据库系统等,自身都提供了比较完善的同步锁机制。我们不用另外给这些资源加锁,这些资源自己就有锁。
但是,大部分情况下,我们在代码中访问的共享资源都是比较简单的共享对象。这些对象里面没有地方让我们加锁。
读者可能会提出建议:为什么不在每一个对象内部都增加一个新的区域,专门用来加锁呢?这种设计理论上当然也是可行的。问题在于,线程同步的情况并不是很普遍。如果因为这小概率事件,在所有对象内部都开辟一块锁空间,将会带来极大的空间浪费。得不偿失。
于是,现代的编程语言的设计思路都是把同步锁加在代码段上。确切的说,是把同步锁加在“访问共享资源的代码段”上。这一点一定要记住,同步锁是加在代码段上的。
同步锁加在代码段上,就很好地解决了上述的空间浪费问题。但是却增加了模型的复杂度,也增加了我们的理解难度。
现在我们就来仔细分析“同步锁加在代码段上”的线程同步模型。
首先,我们已经解决了同步锁加在哪里的问题。我们已经确定,同步锁不是加在共享资源上,而是加在访问共享资源的代码段上。
其次,我们要解决的问题是,我们应该在代码段上加什么样的锁。这个问题是重点中的重点。这是我们尤其要注意的问题:访问同一份共享资源的不同代码段,应该加上同一个同步锁;如果加的是不同的同步锁,那么根本就起不到同步的作用,没有任何意义。
这就是说,同步锁本身也一定是多个线程之间的共享对象。
Java语言的synchronized关键字
为了加深理解,举几个代码段同步的例子。
不同语言的同步锁模型都是一样的。只是表达方式有些不同。这里我们以当前最流行的Java语言为例。Java语言里面用synchronized关键字给代码段加锁。整个语法形式表现为
synchronized(同步锁){
//访问共享资源,需要同步的代码段
}
这里尤其要注意的就是,同步锁本身一定要是共享的对象。
… f1(){
Object lock1= new Object();//产生一个同步锁
synchronized(lock1){
//代码段 A
//访问共享资源 resource1
//需要同步
}
}
上面这段代码没有任何意义。因为那个同步锁是在函数体内部产生的。每个线程调用这段代码的时候,都会产生一个新的同步锁。那么多个线程之间,使用的是不同的同步锁。根本达不到同步的目的。
同步代码一定要写成如下的形式,才有意义。
public static final Object lock1= new Object();
… f1(){
synchronized(lock1){// lock1是公用同步锁
//代码段 A
//访问共享资源 resource1
//需要同步
}
你不一定要把同步锁声明为static或者public,但是你一定要保证相关的同步代码之间,一定要使用同一个同步锁。
讲到这里,你一定会好奇,这个同步锁到底是个什么东西。为什么随便声明一个Object对象,就可以作为同步锁?
在Java里面,同步锁的概念就是这样的。任何一个Object Reference都可以作为同步锁。我们可以把Object Reference理解为对象在内存分配系统中的内存地址。因此,要保证同步代码段之间使用的是同一个同步锁,我们就要保证这些同步代码段的synchronized关键字使用的是同一个Object Reference,同一个内存地址。这也是为什么我在前面的代码中声明lock1的时候,使用了final关键字,这就是为了保证lock1的Object Reference在整个系统运行过程中都保持不变。
一些求知欲强的读者可能想要继续深入了解synchronzied(同步锁)的实际运行机制。Java虚拟机规范中(你可以在google用“JVM Spec”等关键字进行搜索),有对synchronized关键字的详细解释。synchronized会编译成 monitor enter,… monitor exit之类的指令对。Monitor就是实际上的同步锁。每一个Object Reference在概念上都对应一个monitor。
这些实现细节问题,并不是理解同步锁模型的关键。我们继续看几个例子,加深对同步锁模型的理解。
public static final Object lock1= new Object();
… f1(){
synchronized(lock1){// lock1是公用同步锁
//代码段 A
//访问共享资源 resource1
//需要同步
}
}
… f2(){
synchronized(lock1){// lock1是公用同步锁
//代码段 B
//访问共享资源 resource1
//需要同步
}
}
上述的代码中,代码段A和代码段B就是同步的。因为它们使用的是同一个同步锁lock1。
如果有10个线程同时执行代码段A,同时还有20个线程同时执行代码段B,那么这30个线程之间都是要进行同步的。
这30个线程都要竞争一个同步锁lock1。同一时刻,只有一个线程能够获得lock1的所有权,只有一个线程可以执行代码段A或者代码段B。其他竞争失败的线程只能暂停运行,进入到该同步锁的就绪(Ready)队列。
每一个同步锁下面都挂了几个线程队列,包括就绪(Ready)队列,待召(Waiting)队列等。比如,lock1对应的就绪队列就可以叫做lock1- ready queue。每个队列里面都可能有多个暂停运行的线程。
注意,竞争同步锁失败的线程进入的是该同步锁的就绪(Ready)队列,而不是后面要讲述的待召队列(Waiting Queue,也可以翻译为等待队列)。就绪队列里面的线程总是时刻准备着竞争同步锁,时刻准备着运行。而待召队列里面的线程则只能一直等待,直到等到某个信号的通知之后,才能够转移到就绪队列中,准备运行。
成功获取同步锁的线程,执行完同步代码段之后,会释放同步锁。该同步锁的就绪队列中的其他线程就继续下一轮同步锁的竞争。成功者就可以继续运行,失败者还是要乖乖地待在就绪队列中。
因此,线程同步是非常耗费资源的一种操作。我们要尽量控制线程同步的代码段范围。同步的代码段范围越小越好。我们用一个名词“同步粒度”来表示同步代码段的范围。
同步粒度
在Java语言里面,我们可以直接把synchronized关键字直接加在函数的定义上。
比如。
… synchronized… f1(){
// f1代码段
}
这段代码就等价于
… f1(){
synchronized(this){//同步锁就是对象本身
// f1代码段
}
}
同样的原则适用于静态(static)函数
比如。
… static synchronized… f1(){
// f1代码段
}
这段代码就等价于
…static… f1(){
synchronized(Class.forName(…)){//同步锁是类定义本身
// f1代码段
}
}
但是,我们要尽量避免这种直接把synchronized加在函数定义上的偷懒做法。因为我们要控制同步粒度。同步的代码段越小越好。synchronized控制的范围越小越好。
我们不仅要在缩小同步代码段的长度上下功夫,我们同时还要注意细分同步锁。
比如,下面的代码
public static final Object lock1= new Object();
… f1(){
synchronized(lock1){// lock1是公用同步锁
//代码段 A
//访问共享资源 resource1
//需要同步
}
}
… f2(){
synchronized(lock1){// lock1是公用同步锁
//代码段 B
//访问共享资源 resource1
//需要同步
}
}
… f3(){
synchronized(lock1){// lock1是公用同步锁
//代码段 C
//访问共享资源 resource2
//需要同步
}
}
… f4(){
synchronized(lock1){// lock1是公用同步锁
//代码段 D
//访问共享资源 resource2
//需要同步
}
}
上述的4段同步代码,使用同一个同步锁lock1。所有调用4段代码中任何一段代码的线程,都需要竞争同一个同步锁lock1。
我们仔细分析一下,发现这是没有必要的。
因为f1()的代码段A和f2()的代码段B访问的共享资源是resource1,f3()的代码段C和f4()的代码段D访问的共享资源是resource2,它们没有必要都竞争同一个同步锁lock1。我们可以增加一个同步锁lock2。f3()和f4()的代码可以修改为:
public static final Object lock2= new Object();
… f3(){
synchronized(lock2){// lock2是公用同步锁
//代码段 C
//访问共享资源 resource2
//需要同步
}
}
… f4(){
synchronized(lock2){// lock2是公用同步锁
//代码段 D
//访问共享资源 resource2
//需要同步
}
}
这样,f1()和f2()就会竞争lock1,而f3()和f4()就会竞争lock2。这样,分开来分别竞争两个锁,就可以大大较少同步锁竞争的概率,从而减少系统的开销。
信号量
同步锁模型只是最简单的同步模型。同一时刻,只有一个线程能够运行同步代码。
有的时候,我们希望处理更加复杂的同步模型,比如生产者/消费者模型、读写同步模型等。这种情况下,同步锁模型就不够用了。我们需要一个新的模型。这就是我们要讲述的信号量模型。
信号量模型的工作方式如下:线程在运行的过程中,可以主动停下来,等待某个信号量的通知;这时候,该线程就进入到该信号量的待召(Waiting)队列当中;等到通知之后,再继续运行。
很多语言里面,同步锁都由专门的对象表示,对象名通常叫Monitor。
同样,在很多语言中,信号量通常也有专门的对象名来表示,比如,Mutex,Semphore。
信号量模型要比同步锁模型复杂许多。一些系统中,信号量甚至可以跨进程进行同步。另外一些信号量甚至还有计数功能,能够控制同时运行的线程数。
我们没有必要考虑那么复杂的模型。所有那些复杂的模型,都是最基本的模型衍生出来的。只要掌握了最基本的信号量模型——“等待/通知”模型,复杂模型也就迎刃而解了。
我们还是以Java语言为例。Java语言里面的同步锁和信号量概念都非常模糊,没有专门的对象名词来表示同步锁和信号量,只有两个同步锁相关的关键字——volatile和synchronized。
这种模糊虽然导致概念不清,但同时也避免了Monitor、Mutex、Semphore等名词带来的种种误解。我们不必执着于名词之争,可以专注于理解实际的运行原理。
在Java语言里面,任何一个Object Reference都可以作为同步锁。同样的道理,任何一个Object Reference也可以作为信号量。
Object对象的wait()方法就是等待通知,Object对象的notify()方法就是发出通知。
具体调用方法为
(1)等待某个信号量的通知
public static final Object signal= new Object();
… f1(){
synchronized(singal){//首先我们要获取这个信号量。这个信号量同时也是一个同步锁
//只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
signal.wait();//这里要放弃信号量。本线程要进入signal信号量的待召(Waiting)队列
//可怜。辛辛苦苦争取到手的信号量,就这么被放弃了
//等到通知之后,从待召(Waiting)队列转到就绪(Ready)队列里面
//转到了就绪队列中,离CPU核心近了一步,就有机会继续执行下面的代码了。
//仍然需要把signal同步锁竞争到手,才能够真正继续执行下面的代码。命苦啊。
…
}
}
需要注意的是,上述代码中的signal.wait()的意思。signal.wait()很容易导致误解。signal.wait()的意思并不是说,signal开始wait,而是说,运行这段代码的当前线程开始wait这个signal对象,即进入signal对象的待召(Waiting)队列。
(2)发出某个信号量的通知
… f2(){
synchronized(singal){//首先,我们同样要获取这个信号量。同时也是一个同步锁。
//只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
signal.notify();//这里,我们通知signal的待召队列中的某个线程。
//如果某个线程等到了这个通知,那个线程就会转到就绪队列中
//但是本线程仍然继续拥有signal这个同步锁,本线程仍然继续执行
//嘿嘿,虽然本线程好心通知其他线程,
//但是,本线程可没有那么高风亮节,放弃到手的同步锁
//本线程继续执行下面的代码
…
}
}
需要注意的是,signal.notify()的意思。signal.notify()并不是通知signal这个对象本身。而是通知正在等待signal信号量的其他线程。
以上就是Object的wait()和notify()的基本用法。
实际上,wait()还可以定义等待时间,当线程在某信号量的待召队列中,等到足够长的时间,就会等无可等,无需再等,自己就从待召队列转移到就绪队列中了。
另外,还有一个notifyAll()方法,表示通知待召队列里面的所有线程。
这些细节问题,并不对大局产生影响。
绿色线程
绿色线程(Green Thread)是一个相对于操作系统线程(Native Thread)的概念。
操作系统线程(Native Thread)的意思就是,程序里面的线程会真正映射到操作系统的线程,线程的运行和调度都是由操作系统控制的
绿色线程(Green Thread)的意思是,程序里面的线程不会真正映射到操作系统的线程,而是由语言运行平台自身来调度。
当前版本的Python语言的线程就可以映射到操作系统线程。当前版本的Ruby语言的线程就属于绿色线程,无法映射到操作系统的线程,因此Ruby语言的线程的运行速度比较慢。
难道说,绿色线程要比操作系统线程要慢吗?当然不是这样。事实上,情况可能正好相反。Ruby是一个特殊的例子。线程调度器并不是很成熟。
目前,线程的流行实现模型就是绿色线程。比如,stackless Python,就引入了更加轻量的绿色线程概念。在线程并发编程方面,无论是运行速度还是并发负载上,都优于Python。
另一个更著名的例子就是ErLang(爱立信公司开发的一种开源语言)。
ErLang的绿色线程概念非常彻底。ErLang的线程不叫Thread,而是叫做Process。这很容易和进程混淆起来。这里要注意区分一下。
ErLang Process之间根本就不需要同步。因为ErLang语言的所有变量都是final的,不允许变量的值发生任何变化。因此根本就不需要同步。
final变量的另一个好处就是,对象之间不可能出现交叉引用,不可能构成一种环状的关联,对象之间的关联都是单向的,树状的。因此,内存垃圾回收的算法效率也非常高。这就让ErLang能够达到Soft Real Time(软实时)的效果。这对于一门支持内存垃圾回收的语言来说,可不是一件容易的事情。
好了,文章到这里就结束啦,如果本次分享的java什么是交叉引用和Java 多线程 资源冲突问题对您有所帮助,还望关注下本站哦!