上周在线上出现出现报警,ID号码一直无法获取,但是只有这一台机器报警,所以第一时间先在服务治理平台上禁用掉这台机器保证服务正常。停掉机器后要排查问题,思考分析步骤如下:
1 | "Thread-Segment-Update-4" daemon prio=10 tid=0x00007f2c6000c000 nid=0x2455 runnable [0x00007f2c55deb000] |
不理解上面的排查步骤没关系,理解成一句话就是:一个数据库操作的线程一直处于runable状态但是一直hand住没有返回值
通过jstack的信息,可以发现是我们的一个事务在执行conn,setAutoCommit(true)时,一直在native方法read…长时间read。我们有设置超时时间,但是为什么这里会没有超时呢(发现时线程大概运行了有20min)???。咨询DBA同事定位问题之后,大概得出是因为我们没有设置socketTimeout。如果没有设置socketTimeout将会依赖OS底层的timeout,线上大概是30min。虽然通过DBA同事的经验解决了问题,但是仍然存在疑问,为什么mysql存在两种timeout机制呢?queryTimeout和socketTimeout?socketTimeout难道不应该是queryTimeout的子集?queryTimeout应该也能发现我们的Sql超时了啊?
这就和JDBC的实现机制有关系了,为什么会有两种Timeout机制的存在。用一种超时不能解决问题吗?
这个名词也就是queryTimeout,他是属于应用层面的timeout机制。用来控制我们sql语句执行的时间的超时,但是mysql并没有用他来发现所有的问题。下面摘抄一段Statement执行SQL的代码片段
1 |
|
Timer中的代码大致的意思就是copy一个和现在相同的connection,然后执行一条cancelStmt.execute(“KILL QUERY “ + CancelTask.this.connectionId); 语句给数据库,让数据库立刻停止这个链接的SQL
那么整个SQL执行的过程可以简单的描述如下:
这里值得注意的地方,为什么不直接用StatementTimeout直接发现超时,然后返回,不用管rs的结果到底是什么。为了防止超长时间的SQL在server端执行,JDBC在发现自己的statement超时之后,会发一个kill指令给server,那么这个server指令有超时时间吗?有timer吗?在最开始我们代码hang住的setAutoCommit()指令有timer吗?
没有!!!!
下面是通过jprofiler分别执行statement和执行setautoCommit两个指令的内存状态图,可以发现,后者是没有timer对象的(第二张),而前者有(第一张)!!!因为后者的代码根本就没有创建timer逻辑的部分。可以在源码里面看到后者会直接就调用底层的connectionImpl的execSQL方法执行SQL
但是kill是通过copy链接来发送kill命令的。会有timer吗?下图是在发送kill指令时,用debug可以看到,kill 指令发送的时候statementTimeout是0,是不会创建TImer的 ,第一张图是在执行Statement SQL的,超时时间是我们设置的2s,第二张图是执行kill指令时的,可以发现超时时间是0,不会创建timer。仔细想想也能明白,如果kill指令也有timer,逻辑和statement sql一样,那岂不是会无限循环!!
socketTimeout设置得比StatementTimeOut小为1s,后者为5s执行如下语句:
1 | <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> |
会在1s之后得到如下结果,看起来很正常,在timer没有发起kill之前,因为socket没有得到数据所以socket超时了,这一点提醒我们,设置socket超时一定要比statement长,不然你设置得statement超时将毫无意义
1 | The last packet successfully received from the server was 1,057 milliseconds ago. The last packet sent successfully to the server was 1,012 milliseconds ago. |
把statement设置为2s,socketTime设置成6s。会在2s之后得到如下输出。如果socketTime设置成比statement大,那么在后者超时之后,会去kill掉SQL之后立马返回抛出异常
1 | com.mysql.jdbc.exceptions.MySQLTimeoutException: Statement cancelled due to timeout or client request |
和第二种相同的情况,把statement的超时设置成5s,socket超时设置成15s。但是我会在发起kill之前把网络给断掉,来模拟出现网络问题,或者server直接down掉的情况。会在15s后得到一下结果
1 | The last packet successfully received from the server was 15,003 milliseconds ago. The last packet sent successfully to the server was 15,004 milliseconds ago. |
为什么是15s后呢?为什么不是statement的超时的时间呢?这就和上面JDBC源码部分有关系了。从上面的代码可以看出,rs必须返回之后才会抛出异常,当rs不返回时不会继续往下走的。rs什么时候返回?关于kill query什么时候返回,在网上找的一些资料 https://dev.mysql.com/doc/refman/5.7/en/kill.html
注:只针对innodb引擎
mysql KILL QUERY只abort线程当前提交执行的操作,其他的保持不变,并且db server报SQL 语法异常(You have an error in your SQL syntax)。
根据当前被kill的statement是否在事务中,分两种情况分析:
(2) 在事务中
假设事务中sql执行顺序是sql1;sql2;sql3; 在执行sql2时被kill掉,则sql2会抛出异常,并且sql2执行失败。但是sql3依旧会执行下去。此时如果在spring层做了事务回滚处理,会对三条sql全部回滚掉。
sql抛出的异常时MySQLSyntaxErrorException,我们会看到它是受检异常,但是我们了解spring默认是只对非受检异常做回滚处理的,怎么会这样呢?是框架对其做了转化,最终转为非受检异常,spring事物管理器就可以对其做回滚处理了。
KILL操作后,该线程上会设置一个特殊的 kill标记位。通常需要一段时间后才能真正关闭线程,因为kill标记位只在特定的情况下才检查。具体时机是
所以,当kill query这条指令发送过去的时候,由于网络问题一直没响应,会等到socketTimeout之后,整个SQL语句的执行才会返回。所以socketTimeout也不宜设置得太长,在网络不好的时候超时时间基本上不会是statementTimeout的时长。这也就证明了jstack中的问题,那一时刻出现了网络问题,到时setAutoCommit这条指令被卡住,由于没有设置socket超时,得依赖os底层的socket超时时间30min,其实如果我们不重启服务,相信30min钟后服务会自愈。
最好大家自己执行一下上面三种情况,就能很快理解
http://imysql.com/2014/08/13/mysql-faq-howto-shutdown-mysqld-fulgraceful.shtml
http://www.importnew.com/2466.html
https://dev.mysql.com/doc/refman/5.7/en/kill.html
上周在线上出现出现报警,ID号码一直无法获取,但是只有这一台机器报警,所以第一时间先在服务治理平台上禁用掉这台机器保证服务正常。停掉机器后要排查问题,思考分析步骤如下:
线上服务有一台机器访问不通(一个管理平台),在公司的服务治理平台上查看服务的状况是正常的,说明进程还在。进程并没有完全crash掉。去线上查看机器日志,发现了大量的OOM异常:
1 | 017-03-15 00:00:00.041 [WARN] qtp1947699202-120772 nio handle failed |
可以发现是Direct buffer memory的native memory满了,无法分配堆外内存。由于jetty使用的是nio,nio里面大量的使用native memeory。无法分配native memory之后,导致所有的请求都无效
赶快去看了下监控系统上面内存和线程的一些监控指标
文件描述符、runable线程数 、directbuffer 已经达到了2G(我们-Xmx的值),老年代已经1G多了。但是没有fullGC。youngc次数也不是很多,但是出现问题的机器young gc明显比没出现问题的机器多
根据上面的指标,初步定位是由线程创建的网络连接造成了native memory不够 。最上面的log日志显示除了jetty之外还有一个就是ElasticSearch client的worker线程也出现的分配OOM。回想上次开发新功能的时候,会创建大量的ES client连接,会不会是这个问题。查看代码发现确实是因为创建了大量的ES连接,但是并没有主动close掉。导致线程数暴增,由于ElasticSearch client是用的netty做的网络层,使用了大量的DirectByteBuffer.引用一直存在(client线程一直存在),无法GC掉,DirectByteBuffer对象,导致native memory也无法回收。下图是用jprofile测试ElasticSearch client的时候发现的。可以得到的是ElasticSearch client确实会产生大量的DirectByteBuffer
那为什么old gen的对象这么多呢?其实就是一大堆的bytebuffer冰山对象进入了oldgen ,然而fullgc都没有发生,bytebuffer不会被回收,但是这个时候native memory已经被分配完了,所以OOM了。查了资料发现我们在JVM参数中添加了这个:-XX:+DisableExplicitGC.这个参数会导致显示调用System.gc()无效。然而在分配native memory中有这样一段代码:
1 | // These methods should be called whenever direct memory is allocated or |
通过这段代码发现,每次分配native memory的时候会通知jvm进行一次GC。如果我们配置了上面的jvm参数会导致这行代码不起任何作用。old gen里面的对象就算死掉也不会回收,除非old gen本身满了,但是通过上面的old gen使用了1G,还没有到old gen的最大值。没有full gc所以native memory一直没有被回收。其实就算我们没有设置jvm参数估计也会OOM ,因为bytebuffer因为线程只有的关系不会全部死掉。大部分bytebuffer也不会被回收
和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。堆外内存默认是和-Xmx默认一样大,也可以使用-XX:MaxDirectMemorySize指定堆外内存大小
作为JAVA开发者我们经常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作。关于Cleaner回收native memory又是另一个重要的点了。比如:为什么不用finalize来回收native memory。另外找机会写.
我们可以知道的是,随着DirectByteBuffer被GC掉之后,被分配的native memory会被回收
1 | DirectByteBuffer(int cap) { |
我们知道了回收堆内的DriectByteBuffer就会回收native memory,出现OOM的情况就是native memory被分配完了。也同时因为这个原因,假设进入Old gen的对象本来已经死了,但是并没有full gc回收,native memory不能被及时回收。为了避免这种情况,在分配DirectByteByffer的时候会主动调用一次System.GC().通知JVM进行一次full gc。定期清理堆中的垃圾,及时的释放native memory。
但是用了-XX:+DisableExplicitGC参数后,System.gc()的调用就会变成一个空调用,完全不会触发任何GC(但是“函数调用”本身的开销还是存在的).为啥要用这个参数呢?最主要的原因是为了防止某些手贱的同学在代码里到处写System.gc()的调用而STW干扰了程序的正常运行吧。有些应用程序本来可能正常跑一天也不会出一次full GC,但就是因为有人在代码里调用了System.gc()而不得不间歇性被暂停。也有些时候这些调用是在某些库或框架里写的,改不了它们的代码但又不想被这些调用干扰也会用这参数。
但是如果使用堆外内存,同时使用了这个参数,比如同时满足下面3个条件,就很容易发生OOM
下面使用一些实例在看看在使用了DisableExplicitGC这个参数之后,到底会发生什么。
native memory满了,但是young区没满,没有发生young gc回收DirectByteBuffer,所以堆外OOM(如果去掉DisableExplicitGC参数程序会一直有Full GC的信息输出,因为分配native memory的时候会主动调用System.GC())
1 | -Xmx64m |
native memory没满,但是young区在native memory满之前提前满了,发生young gc回收DirectByteBuffer,不会发生OOM
如果代码换成了下面这种(jvm参数一样),一次分配的native memory足够小,会导致在native memory没有分配满的情况下,发生young gc会搜DirectByteBuffer。同时会回收native memory
1 | ByteBuffer byteBuffer = ByteBuffer.allocateDirect( 1024/2/2/2/2/2); |
大量DirectByteBuffer对象移动到old gen。没有Full gc的发生,导致在程序中可能死掉的DirectByteBuffer对象没有回收掉,native memory则满了,发生OOM
1 |
|
如果你在使用Oracle/Sun JDK 6,应用里有任何地方用了direct memory,那么使用-XX:+DisableExplicitGC要小心。如果用了该参数而且遇到direct memory的OOM,可以尝试去掉该参数看是否能避开这种OOM
先写到这里,关于DirectByteBuffer的Cleaner方式如何回收Native memory方面,以后在添加(微笑)
线上服务有一台机器访问不通(一个管理平台),在公司的服务治理平台上查看服务的状况是正常的,说明进程还在。进程并没有完全crash掉。去线上查看机器日志,发现了大量的OOM异常:
什么是代理,在Design patterns In java
这个本书中是这样描述的,简单的说就是为某个对象提供一个代理,以控制对这个对象的访问。在不修改源代码的基础上做方法增强,代理是一种设计模式,又简单的分为两种。
先来看一个静态代理的例子,Calculator是一个计算器的接口类,定义了一个加法的接口方法,由CalculatorImpl类实现真正的加法操作.现在如果我们想对这个方法做一层静态的代理,这儿实现了一个简单的代理类实现了计算接口Calculator,构造函数传入的参数是真正的实现类,但是在调用这个代理类的add方法的时候我们在CalculatorImpl的实现方法执行的前后分别做了一些操作。这样的代理方式就叫做静态代理(可以理解成一个简单的装饰模式)。
很明显静态代理的缺点,由于我们需要事先实现代理类,那么每个方法我都都需要去实现。如果我们要实现很多的代理类,那么工作量就太大了。动态代理的产生就是这样而来的。
1 | public interface Calculator { |
使用动态代理可以让代理类在程序运行的时候生成代理类,我们只需要为一类代理写一个具体的实现类就行了,所以实现动态代理要比静态代理简单许多,省了不少重复的工作。在JDK的方案中我们只需要这样做可以实现动态代理了。
1 | public class ProxyFactory implements InvocationHandler { |
利用JDK的proxy实现代理动态代理,有几个关键点,一个就是InvocationHandler接口,这个方法中的invoke方法是执行代理时会执行的方法。所以我们所有代理需要执行的逻辑都会写在这里面,invo参数里面的method可以使用java 反射调用真实的实现类的方法,我们在这个方法周围做一些代理逻辑工作就可以了。上面的代码会把Calculator接口的所有方法全部在程序运行时代理。不用我们一个个的去写静态代理的方法。
先看Proxy.newProxyInstance(...)
方法中的具体实现(省略大部分方法)。在下面的代码中会通过getProxyClass0(…)方法得到class对象,然后给把InvocationHandler已构造参数实例化代理对象。思路还是挺清晰的,但是如果要一探究竟我们还是得知道代理对象到底是什么样的,如何实现的代理呢?
1 | public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException |
用ProxyGenerator.generateProxyClass(..)
方法生成字节流,然后写进硬盘.假设我把proxyName定义为Calcultor$ProxyCode
.我们先在https://bitbucket.org/mstrobel/procyon/downloads 下载一个反编译的jar包。然后运行下面的代码,我们得到了一个Calcultor$ProxyCode.class
的文件.然后在目录下使用命令java -jar procyon-decompiler-0.5.29.jar Calcultor$ProxyCode.class
就能得到Calcultor$ProxyCode.java
文件。 当然也可以实现在线反编译http://javare.cn/网站反编译然后下载文件
1 | public static void main(String[] args) { |
下面的代码是就是反编译过来的Calcultor$ProxyCode
类。可以发现这个类实现了我们需要代理的接口Calculator
。且他的构造函数确实是需要传递一个InvocationHandler
对象,那么现在的情况就是我们的生成了一个代理类,这个代理类是我们需要代理的接口的实现类。我们的接口中定义了add和reduce方法,在这个代理类中帮我们实现了,并且全部变成了final的。同时覆盖了一些Object类中的方法。那我们现在以reduce这个方法举例,方法中会调用InvocationHandler
类中的invoke方法(也就是我们实现的逻辑的地方)。同时把自己的Method
对象,参数列表等传入进去。
1 | public final class Calcultor$ProxyCode extends Proxy implements Calculator { |
现在我们对JDK代理有个简单的源码级别的认识,理清楚一下思路:JDK会帮我们在运行时生成一个代理类,这个代理类实际上就是我们需要代理的接口的实现类。实现的方法里面会调用InvocationHandler
类中的invoke方法,并且同时传入自身被调用的方法的的Method对象和参数列表方便我们编码实现方法的调用。比如我们调用reduce方法,那么我们就可以通过Method.Invoke(Object obj, Object... args)
调用我们具体的实现类,再在周围做一些代理做的事儿。就实现了动态代理。我们对JDK的特性做一些简单的认识:
equals、hashCode、toString
,它们都只是简单的调用了InvocationHandler的invoke方法,即可以对其进行特殊的操作,也就是说JDK的动态代理还可以代理上述三个方法Method.invoke
方法调用实现类就返回。这种方式常常用在RPC
框架中,在invoke方法中发起通信调用远端的接口等JDK中提供的生成动态代理类的机制有个鲜明的特点是:某个类必须有实现的接口,而生成的代理类也只能代理某个类接口定义的方法。那么如果一个类没有实现接口怎么办呢?这就有CGLIB
的诞生了,前面说的JDK的代理类的实现方式是实现相关的接口成为接口的实现类,那么我们自然而然的可以想到用继承的方式实现相关的代理类。CGLIB就是这样做的。一个简单的CGLIB代理是这样实现的:
1 | Enhancer enhancer=new Enhancer(); |
通过在执行动态代理的代码前面加上一行代码就可以得到生成的代理对象.代理对象的class文件会生成在你定义的路径下。类似Calculator$CalculatorImpl$$EnhancerByCGLIB$$58419779.class
这样结构。
1 | System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "${your path}"); |
之后我们通过反编译得到反编译后的java文件。就上面的例子而言我们传入的superclass是一个接口,并不是实现类。那我们得到的代理类会长成这样:
1 | //如果是接口代理类还是通过实现接口的方式 |
如果传入的并不是接口,而是实现类的话,就会得到下面的代理类:
1 | //如果是普通的类,会采用继承的方式实现 |
但是不管是传入的接口还是传入的代理类,代码的实体都是长得差不多的:
1 | ublic class Calculator$CalculatorImpl$$EnhancerByCGLIB$$2849428a extends CalculatorImpl implements Factory { |
上面这一份代码整个代理的流程仿佛是差不多的,都是在调用方法的时候router到InvokeHandler或者MethodInterceptor。为什么会有两种呢,因为CGLIB提供了filter的机制,可以让不同的方法代理到不同的callback中,如下面这样:
1 | enhancer.setCallbacks(new Callback[]{new MethodInterceptor() { |
这两种callback不一样的地方很显而易见, MethodInterceptor的方法参数多了一个MethodProxy对象,在使用这个对象的时候的时候有两个方法可以让我们调用:
1 | public Object invoke(Object obj, Object[] args) throws Throwable { |
FastClass是Cglib实现的一种通过给方法建立下标索引来访问方法的策略,为了绕开反射。
上面的描述代表MethodPeoxy可以根据对方法建立索引调用方法,而不需要使用传统Method的invoke反射调用,提高了性能,当然额外的得多生成一些类信息,比如在最开始的代理类中我们也可以看到MethodProxy也是有通过索引来做的,这样的话做到了FastClass,FastClass大致是这样实现的:
1 | class FastTest { |
所以在使用MethodInterceptor的时候可以这样使用:
1 | public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { |
编写好ASpectj文件之后,编译代码就能够得到静态织入的class文件了,接下来简单的介绍一下AspectJ是在哪个地方植入代码到class文件的.
反编译过后得到的java代码如下:
1 | @RequestMapping({"/hello"}) |
上面两个方法都实现了@ RequestMapping注解,类也实现类Mtrace接口。但是因为传入参数的类型不同,所以只有第一个方法被织入了代理的方法,在真正的方法快周围分表调用了before
、after
、afterThrowing
、afterRutnrn
等方法。Aspectj简单的原理就是这样.更加深入的原理解析暂时就不做了。
private
、或者static
、或者final
的,都可以代理toString()
,clone()
等方法Spring代理实际上是对JDK代理和CGLIB代理做了一层封装,并且引入了AOP概念:Aspect、advice、joinpoint等等,同时引入了AspectJ中的一些注解@pointCut
,@after
,@before
等等.Spring Aop严格的来说都是动态代理,所以实际上Spring代理和Aspectj的关系并不大.
Spring代理中org.springframework.aop.framework.ProxyFactory
是关键,一个简单的使用API编程的Spring AOP代理如下:
1 | ProxyFactory proxyFactory =new ProxyFactory(Calculator.class, new MethodInterceptor() { |
在调用getProxy()
时,会优先得到一个默认的DefaultAopProxyFactory
.这个类主要是决定到底是使用JDK代理还是CGLIB代理:
1 |
|
那么JdkDynamicAopProxy中的invoke方法就是最核心的方法了(实现了InvokeHandler接口):
1 |
|
下面来分析整个代理的拦截器是怎么运行的,ReflectiveMethodInvocation
这个类的proceed()
方法负责递归调用所有的拦截的织入。
1 | public Object proceed() throws Throwable { |
那么要实现织入,只需要控制织入的代码和调用proceed
方法的位置,在Spring中的before
织入是这样实现的:
1 | public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable { |
afterRuturning
是这样实现的:
1 | public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable { |
下面这幅流程图是一个一个包含上述一个before织入和一个afterReturning织入的流程图:
要实现这种环绕的模式其实很简单,下面提供一个最简单的实现,利用迭代的思想很简单的实现了链式调用。并且可扩展性非常高。和AspectJ的直接静态织入改变代码结构的方式来分别织入before、after等来说。这种方式设计更优雅。但是在SpringMVC
中拦截器却并不是这种方式实现的,哈哈。
1 | public interface MethodInterceptor { |
1 | public interface Invocation { |
Spring AOP封装了JDK和CGLIB的动态代理实现,同时引入了AspectJ的编程方式和注解。使得可以使用标准的AOP编程规范来编写代码外,还提供了多种代理方式选择。可以根据需求来选择最合适的代理模式。同时Spring也提供了XML配置的方式实现AOP配置。可以说是把所有想要的都做出来了,Spring是在平时编程中使用动态代理的不二选择.
]]>什么是代理,在Design patterns In java
这个本书中是这样描述的,简单的说就是为某个对象提供一个代理,以控制对这个对象的访问。在不修改源代码的基础上做方法增强,代理是一种设计模式,又简单的分为]]>
在本博文的一篇如何正确的关闭一个线程
一文中讲解了如何利用interrupt机制来中断一个线程,这篇文章当时确实花了一些精力的总结,不过都是15年末的事情了,现在是2017年2月份,经过一年的时间,决定重新写一篇完善的关于线程中断的文章。
下面简单的举例情况:
Thread.STOP()
之类的api会造成一些不可预知的bug,所以很早便Deprecated
了,真要纠结为什么请看这边文章为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?
Thread类定义了如下关于中断的方法:
API | 作用 |
---|---|
public static boolean interrupted |
就是返回对应线程的中断标志位是否为true返回当前线程的中断标志位是否为true,但它还有一个重要的副作用,就是清空中断标志位,也就是说,连续两次调用interrupted(),第一次返回的结果为true,第二次一般就是false (除非同时又发生了一次中断)。 |
public boolean isInterrupted() |
就是返回对应线程的中断标志位是否为true |
public void interrupt() |
表示中断对应的线程 |
如果线程在运行中,interrupt()只是会设置线程的中断标志位,没有任何其它作用。线程应该在运行过程中合适的位置检查中断标志位,比如说,如果主体代码是一个循环,可以在循环开始处进行检查,如下所示:
1 | public class InterruptRunnableDemo extends Thread { |
线程执行如下方法会进入WAITING状态:
1 | public final void join() throws InterruptedException |
执行如下方法会进入TIMED_WAITING状态:
1 | public final native void wait(long timeout) throws InterruptedException; |
在这些状态时,对线程对象调用interrupt()会使得该线程抛出InterruptedException,需要注意的是,抛出异常后,中断标志位会被清空(线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。),而不是被设置。比如说,执行如下代码:
1 | Thread t = new Thread (){ |
InterruptedException是一个受检异常,线程必须进行处理。我们在异常处理中介绍过,处理异常的基本思路是,如果你知道怎么处理,就进行处理,如果不知道,就应该向上传递,通常情况下,你不应该做的是,捕获异常然后忽略。
捕获到InterruptedException,通常表示希望结束该线程,线程大概有两种处理方式:
第一种方式的示例代码如下:
1 | //抛出中断异常,由调用者捕获 |
第二种方式的示例代码如下:
1 | public class InterruptWaitingDemo extends Thread { |
如果线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正”中断”。我们看段代码:
1 | public class InterruptWaitingDemo extends Thread { |
BLOCKED
如果线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正”中断”。我们看段代码:
1 | public class InterruptSynchronizedDemo { |
test方法在持有锁lock的情况下启动线程a,而线程a也去尝试获得锁lock,所以会进入锁等待队列,随后test调用线程a的interrupt方法并等待线程线程a结束,线程a会结束吗?不会,interrupt方法只会设置线程的中断标志,而并不会使它从锁等待队列中出来。
我们稍微修改下代码,去掉test方法中的最后一行a.join,即变为:
1 | public static void test() throws InterruptedException { |
这时,程序就会退出。为什么呢?因为主线程不再等待线程a结束,释放锁lock后,线程a会获得锁,然后检测到发生了中断,所以会退出。
在使用synchronized关键字获取锁的过程中不响应中断请求,这是synchronized的局限性。如果这对程序是一个问题,应该使用显式锁,java中的Lock接口,它支持以响应中断的方式获取锁。对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。
如果线程尚未启动(NEW),或者已经结束(TERMINATED),则调用interrupt()对它没有任何效果,中断标志位也不会被设置。比如说,以下代码的输出都是false。
1 | public class InterruptNotAliveDemo { |
如果线程在等待IO操作,尤其是网络IO,则会有一些特殊的处理,我们没有介绍过网络,这里只是简单介绍下。
我们重点介绍另一种情况,InputStream的read调用,该操作是不可中断的,如果流中没有数据,read会阻塞 (但线程状态依然是RUNNABLE),且不响应interrupt(),与synchronized类似,调用interrupt()只会设置线程的中断标志,而不会真正”中断”它,我们看段代码
1 | public class InterruptReadDemo { |
线程t启动后调用System.in.read()从标准输入读入一个字符,不要输入任何字符,我们会看到,调用interrupt()不会中断read(),线程会一直运行。
不过,有一个办法可以中断read()调用,那就是调用流的close方法,我们将代码改为:
1 | public class InterruptReadDemo { |
我们给线程定义了一个cancel方法,在该方法中,调用了流的close方法,同时调用了interrupt方法,这次,程序会输出:
1 | -1 |
也就是说,调用close方法后,read方法会返回,返回值为-1,表示流结束。
再比如,ExecutorService提供了如下两个关闭方法:
1 | void shutdown(); |
Future和ExecutorService的API文档对这些方法都进行了详细说明,这是我们应该学习的方式。
在本博文的一篇如何正确的关闭一个线程
一文中讲解了如何利用interrupt机制来中断一个线程,这篇文章当时确实花了一些精力的总结,不过都是15年末的事情了,现在是2017年2月份,经过一年的时间,决定重新写一篇]]>
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
接下来一次来验证每个隔离级别的特性,首先我们先建一张表,我们建立账户表account用来测试我们的事务隔离级别:
1 | CREATE TABLE account ( |
首先我们开启Console A,然后设置session事务隔离级别为read uncommitted; 然后同样开启Console B,设置成read uncommitted;
1 | mysql> set session transaction isolation level read uncommitted; |
我们两个console的事务隔离级别都是read uncommitted,下面测试RU级别会发生的情况
可以发现RU模式下,一个事务可以读取到另一个未提交(commit)的数据,导致了脏读。如果B事务回滚了,就会造成数据的不一致。RU是事务隔离级别最低的。
现在我们将事务隔离级别设置成RC (read committed)
1 | set session transaction isolation level read uncommitted; |
我们在RC模式下,可以发现。在console B没有提交数据修改的commit的时候,console A是读不到修改后的数据的,这就避免了在RU模式中的脏读,但是有一个问题我们会发现,在console A同一个事务中。两次select的数据不一样,这就存在了不可重复读的问题.PS:RC事务隔离级别是Oracle数据库的默认隔离级别.
在RR级别中,我们解决了不可重复读的问题,即在这种隔离级别下,在一个事务中我们能够保证能够获取到一样的数据(即使已经有其他事务修改了我们的数据)。但是无法避免幻读,幻读简单的解释就是在数据有新增的时候,也无法保证两次得到的数据不一致,但是不同数据库对不同的RR级别有不同的实现,有时候或加上间隙锁来避免幻读。
前面的定义中RR级别是可能产生幻读,这是在传统的RR级别定义中会出现的。但是在innoDB引擎中利用MVCC多版本并发控制解决了这个问题
这算是幻读吗?在标准的RR隔离级别定义中是无法解决幻读问题的,比如我要保证可重复读,那么我们可以在我们的结果集的范围加一个锁(between 1 and 11),防止数据更改.但是我们毕竟不是锁住真个表,所以insert数据我们并不能保证他不插入。所以是有幻读的问题存在的。但是innodb引擎解决了幻读的问题,基于MVCC(多版本并发控制):在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。所以当我们执行update的时候,当前事务的版本号已经更新了?所以也算是幻读??(存疑)主要是gap间隙锁+MVCC解决幻读问题?
所有事物串行,最高隔离级别,性能最差
在RR模型,我们虽然避免了幻读,但是存在一个问题,我们得到的数据不是数据中实时的数据,如果是对实时数据比较敏感的业务,这是不现实的。
对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:
事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”,就需要另外的模块来解决了。
比如,我们有以下的订单业务场景,我们队一个商品下单的操作,我们得首先检查这个订单的数量还剩多少,然后下单。
事务1:
1 | select num from t_goods where id=1; |
事务2:
1 | select num from t_goods where id=1; |
假设这个时候数量只有1,我们下单也是只有1.如果在并发的情况下,事务1查询到还有一单准备下单,但是这个时候事务2已经提交了。订单变成0.这个事务1在执行update,就会造成事故。
1 | select num,version from t_goods where id=1; |
编程式事务就是利用手动代码编写事务相关的业务逻辑,这种方式比较复杂、啰嗦,但是更加灵活可控制(个人比较喜欢)
1 | public void testTransactionTemplate() { |
1 | <tx:advice id="txAdvice" transaction-manager="txManager"> |
1 | <tx:annotation-driven transaction-manager="transactioManager" /><!--开启注解的方式--> |
1 |
|
我们在这里不对两种事务编程做过多的讲解
Spring管理的事务是逻辑事务,而且物理事务和逻辑事务最大差别就在于事务传播行为,事务传播行为用于指定在多个事务方法间调用时,事务是如何在这些方法间传播的,Spring共支持7种传播行为
为了演示事务传播行为,我们新建一张用户表
1 | EATE TABLE user ( |
必须有逻辑事务,否则新建一个事务,使用PROPAGATION_REQUIRED指定,表示如果当前存在一个逻辑事务,则加入该逻辑事务,否则将新建一个逻辑事务,如下图所示;
测试的代码如下,在account插入的地方主动回滚
1 |
|
按照required的逻辑,代码执行的逻辑如下:
所以在这种情况下,两个事务属于同一个事务,一个回滚则两个任务都回滚。
创建新的逻辑事务,使用PROPAGATION_REQUIRES_NEW指定,表示每次都创建新的逻辑事务(物理事务也是不同的)如下图所示:
支持当前事务,使用PROPAGATION_SUPPORTS指定,指如果当前存在逻辑事务,就加入到该逻辑事务,如果当前没有逻辑事务,就以非事务方式执行,如下图所示:
不支持事务,如果当前存在事务则暂停该事务,使用PROPAGATION_NOT_SUPPORTED指定,即以非事务方式执行,如果当前存在逻辑事务,就把当前事务暂停,以非事务方式执行。
必须有事务,否则抛出异常,使用PROPAGATION_MANDATORY指定,使用当前事务执行,如果当前没有事务,则抛出异常(IllegalTransactionStateException)。当运行在存在逻辑事务中则以当前事务运行,如果没有运行在事务中,则抛出异常
不支持事务,如果当前存在是事务则抛出异常,使用PROPAGATION_NEVER指定,即以非事务方式执行,如果当前存在事务,则抛出异常(IllegalTransactionStateException)
嵌套事务支持,使用PROPAGATION_NESTED指定,如果当前存在事务,则在嵌套事务内执行,如果当前不存在事务,则创建一个新的事务,嵌套事务使用数据库中的保存点来实现,即嵌套事务回滚不影响外部事务,但外部事务回滚将导致嵌套事务回滚。
Nested使用JDBC 3的保存点(save point)实现,即如果使用低版本驱动将导致不支持嵌套事务。
使用嵌套事务,必须确保具体事务管理器实现的nestedTransactionAllowed属性为true,否则不支持嵌套事务,如DataSourceTransactionManager默认支持,而HibernateTransactionManager默认不支持,需要设置来开启。
ps:博主水平有限,不妥之处请不吝赐教。转载需注明来自 yaccc.github.io
]]>隔离级别 | 脏读 | 不可重复读]]>
介绍
Threadlocal bug?如果子线程想要拿到父线程的中的ThreadLocal值怎么办呢?比如会有以下的这种代码的实现。由于ThreadLocal的实现机制,在子线程中get时,我们拿到的Thread对象是当前子线程对象,那么他的ThreadLocalMap是 InheritableThreadLocal实现那其实很多时候我们是有子线程获得父线程ThreadLocal的需求的,要如何解决这个问题呢?这就是 以上代码大致的意思就是,如果你使用InheritableThreadLocal,那么保存的所有东西都已经不在原来的t.thradLocals里面,而是在一个新的t.inheritableThreadLocals变量中了。下面是Thread类中两个变量的定义 Q:InheritableThreadLocal是如何实现在子线程中能拿到当前父线程中的值的呢? 而且,在copy过程中是 恩,到了这里,大致的解释了一下
so,在最开始的代码示例中,如果把ThreadLocal对象换成InheritableThreadLocal对象,那么get到的字符会是“xiezhaodong”而不是NULL InheritableThreadLocal还有问题吗?问题场景我们在使用线程的时候往往不会只是简单的new Thrad对象,而是使用线程池,当然线程池的好处多多。这里不详解,既然这里提出了问题,那么线程池会给InheritableThreadLocal带来什么问题呢?我们列举一下线程池的特点:
对于第一点,如果一个子线程已经使用过,并且会set新的值到ThreadLocal中,那么第二个task提交进来的时候还能获得父线程中的值吗?比如下面这种情况(虽然是线程,用sleep尽量让他们串行的执行): 输出的会是 造成这个问题的原因是什么呢,下图大致讲解一下整个过程的变化情况,如图所示,由于B任务提交的时候使用了,A任务的缓存线程,A缓存线程的InheritableThreadLocal中的value已经被更新成了”zhangzhangzhang“。B任务在代码内获得值的时候,直接从t.InheritableThreadLocal中获得值,所以就获得了线程A中心设置的值,而不是父线程中InheritableThreadLocal的值。 造成问题的原因那么造成这个问题的原因是什么呢?如何让任务之间使用缓存的线程不受影响呢?实际原因是,我们的线程在执行完毕的时候并没有清除ThreadLocal中的值,导致后面的任务重用现在的localMap。 解决方案如果我们能够,在使用完这个线程的时候清除所有的localMap,在submit新任务的时候在重新重父线程中copy所有的Entry。然后重新给当前线程的t.inhertableThreadLocal赋值。这样就能够解决在线程池中每一个新的任务都能够获得父线程中ThreadLocal中的值而不受其他任务的影响,因为在生命周期完成的时候会自动clear所有的数据。Alibaba的一个库解决了这个问题github:alibaba/transmittable-thread-local transmittable-thread-local实现原理如何使用这个库最简单的方式是这样使用的,通过简单的修饰,使得提交的runable拥有了上一节所述的功能。具体的API文档详见github,这里不再赘述 原理简述这个方法TtlRunnable.get(task)最终会调用构造方法,返回的是该类本身,也是一个Runable,这样就完成了简单的装饰。最重要的是在run方法这个地方。
在上面的使用线程池的例子当中,如果换成这种修饰的方式进行操作,B任务得到的肯定是父线程中ThreadLocal的值,解决了在线程池中InheritableThreadLocal不能解决的问题。 更新父线程ThreadLocal值?如果线程之间出了要能够得到父线程中的值,同时想更新值怎么办呢?在前面我们有提到,当子线程copy父线程的ThreadLocalMap的时候是浅拷贝的,代表子线程Entry里面的value都是指向的同一个引用,我们只要修改这个引用的同时就能够修改父线程当中的值了,比如这样:
这样父线程中的值就会得到更新了。能够满足父线程ThreadLocal值的实时更新,同时子线程也能共享父线程的值。不过场景倒是不是很常见的样子。 ]]>介绍 为什么要在interceptor层获得方法名称呢?在分布式链路系统中我们需要在MVC框架层埋点,统计方法调用的耗时、trace信息等,目前公司内部没有统一的MVC框架,但是大多数都是使用的SpringMVC.所以我们在Interceptor这一层埋点就ok。在这里可以统计到方法调用完的耗时信息,同时也可以得到用户自定义的埋点信息。在这个过程中踩了一些坑,也尝试了各种方法 Interceptor介绍
该handler是什么呢?通过 高版本SpringMVC(3.1+)那么HandlerExecutionChain是怎么初始化的呢?它是靠HandlerMapping来初始化的,HandlerMapping的实例可以自己配置,或者使用默认配置,SpringMVC会默认的加载DispatcherServlet.properties配置文件中的这几种配置
HandlerMapping的工作就是将request和handler映射起来,但是我们会有多种方式,比如通过controller的名称、或者在xml中配置、又或者使用annotation的方式。所以mapping有很多种,当然也可以配置多个HandlerMapping,SpringMVC通过适配器模式为你找到匹配的HandlerMapping。那么这个Handler究竟是什么呢? 在 但是SpringMVC3.1以上版本
使用了RequestMappingHandlerMapping之后,handler的实例就变成了 低版本SpringMVC(3.1以下)如果是低版本的SpringMVC 那就没办法了,只能拿到Controller实例的对象,这里心生一计,既然能得到Controller对象,是否可以通过request中的url,在通过反射拿到所有方法的注解值然后mapping到方法呢?好想是可以的,但是这里有一个问题,就是url匹配的问题,SpringMVC包含了多种url匹配,比如RESTFUL,还有各种匹配格式,非常繁琐。要么自己重写SpringMVC的匹配,要么就使用内部的匹配方法。这一点也提醒了我,SpringMVC最后肯定会通过一种方式找到对应的方法然后invoke的。这也就是adapter的责任。看看DispatcherServlet(前两个)源码细节
流程大概是这样的:
好了,终于找到了url匹配的方法,这个方法要用两个东西,一个是handler,一个是request。我们要如何使用它呢?由于adapter在拦截器之前执行,所以方法映射都已经初始化完毕了。所以我们只能使用初始化完毕之后的map对象,这里就只有使用反射:大概的代码是这样的。
这样就能完美的得到被调用的方法名称了,回顾一下整个流程,看起来很简单,其实是一个源码探究的过程,SpringMVC整个过程还是非常复杂的,但是扩展性有些地方很好,有些地方却差强人意。这种方式不好的地方就死对Spring使用了反射,这种侵入性还是有一点,不过我验证之后发现,从2.5开始每个版本的AnnotationMethodHandlerAdapter类都有此方法,所以还算合格。还有一个缺点就是目前只正对annotaion方式做了除了,比如基本的SimpleUrlHandlerMapping等暂时还没有做处理。那么在整个途中还延伸了一种AOP的方法 利用AspectJ AOP代理想到拦截器,自然也想到了代理机制,我们使用AOP环绕或者before、after的方式给方法埋点是否更好呢?其实这种方式对Controller层都会织入我们的ASpectJ代码。使用最简单的方式就行给加上Trace注解的方法都织如aop代理:
编译之后,代码大概会是这样:
总结
为什么要在interceptor层获得方法名称呢?在分布式链路系统中我们需要在MVC框架层埋点,统计方法调用的耗时、trace信息等,目前公司内部没有统一的MVC框架,但是大多数都是使用的SpringMVC.所以我们在Intercept]]> 我们来看看作者Doug Lea是怎么说的,下面是jdk7.x里面ThreadLocal注释
也就是说这个类给线程提供了一个本地变量,这个变量是该线程自己拥有的。在该线程存活和ThreadLocal实例能访问的时候,保存了对这个变量副本的引用.当线程消失的时候,所有的本地实例都会被GC。并且建议我们ThreadLocal最好是 private static 修饰的成员 和Thread的关系假设我们要设计一个和线程绑定的变量,我们会怎么做呢?很常见的一个思路就是把Thread和变量放在一个Map
然后,
原来是把ThreadLocalMap和Thread绑定起来了,Thread类中有一个ThreadLocalMap为null的变量,那我们现在回到ThreadLocalMap来看,在我们Thread返回的引用来看,如果map为null的情况下,调用了createMap方法.这就为我们的Thread创建了一个能保存在本地线程的map.下面是Thread里面的字段
ThreadLocalMap那么当我们第一次使用ThreadLocal的时候,我们通过getMAP得到的ThreadLocalMap必然是null,我们来看看createMap方法
在CreatMap中会直接new 一个ThreadLocalMap,里面传入的是当前ThreadLocal#this.然后创建一个大小为INITIAL_CAPACITY的Entry。关于这个INITIAL_CAPACITY为什么是2的N次方,这在HashMap里面也是有体现的,这里INITIAL_CAPACITY为16那么16-1=15在二进制中就是1111.当他和TheadLocal的INITIAL_CAPACITY相与的时候,得到的数绝对是<=INITIAL_CAPACITY.这和 这里小结一下,现在我们应该能够理清ThreadLocal和Thread的关系了,大致是这样的:Thread里面有一个类似MAP的东西,但是初始化的时候为null,当我们使用ThreadLocal的时候,ThreadLocal会帮助当前线程初始化这个MAP,并且把我们需要和线程绑定的值放入改Map中。map的key为当前ThreadLocal。那么这样和我们才开始的想法有什么不一样呢,才开始我们的想法是在ThreadLocal当中维护一个mao,key为Thread表示,value为值。和这样的方式有什么差别呢,为什么要这样做?话说在jdk1.3之前就是用这种方式做的,但是之后就改成了现在的这种做法。这样做法的优点之一是,value放在了线程当中,随着线程的生命周期生存,线程死亡,value回收。之二是性能提高了,想想一下在有很多请求的应用中,如果按照之前的做法,HashMap该多大?,性能应该会比较低,而换成后者这种方法,map的大小变得比较小,和Threadlocal的数量相同(有多少个ThreadLocal,线程当中的map实际存储的就有多少个)。 可能存在的问题上文看似我们已经渐渐的明白了ThreadLocal的本质,实际上Threadlocal可能会存在一些些问题
如下图(摘自网络),ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链,这就会造成很多人都认为的内存泄露,其实我认为是不会发生的。继续看 按照道理来说,如果我们使用的线程池方式,当一个线程使用完的时候,线程并没有死亡,而是回归线程池继续使用,这个时候和该线程的bind其实并有没什么意义呢,但是呢?value并不会被回收,这也算导致了内存泄露,还有一种情况就是上述所说的,当弱引用被回收吊,null无法访问value,也导致了相同的问题。那么?这是真的吗?先卖一个关子,我们先来看看一些其他的东西 ThreadLocal小片段ThreadLocal之间是如何区分的呢?给每个ThreadLocal一个标识符?这确实是一种思路,jdk里面是这样做的。
比如我第一个ThreadLocal的hashCode就是0,那么我在定义一个他的hashCode就是0的基础上加上HASH_INCREMENT。这样在map中他们的hahscode不一样,但是这个时候虽然hashcode不一样,但是计算出来的下标i可能是一样的,这就造成了hash冲突,在ThreadLocal里面用的解决Hash冲突是用的线性探查法(Linear Probing)来解决的,当i下标有值的时候则找到i+1处,然后依次往下推。看看set、get 粗虐上来看,这是一个非常简单的对map的add、get、init操作,但是我们来看看ThreadLocalMap#set方法的一些细节
在get中getEntry()方法通过计算出的下标从table中取出entry,如果取得的entry为null或它的key值不相等,就调用getEntryAfterMiss()方法,否则返回。
总结到了最后,上面我们留下的问题大致也都得到了答案,在我们调用set或者get的时候,ThreadLocal会自动的清楚key为null的值,不会造成内存泄露。而当使用线程池的时候,我们应该在改线程使用完该ThreadLocal的时候自觉地调用remove方法清空Entry,这会是一个非常好的习惯。
我们来看看作者Doug Lea是怎么说的,下面是jdk7.x里面ThreadLocal注释
主要介绍 ThreadPoolExecutor介绍ThreadPoolExecutor的完整构造方法的签名如下
根据上面的描述,我相信我们能够在熟悉参数的情况下自定义自己的线程池,但是我们发现在jdk帮助文档里面有这样一句话
线程池的工作方式
那么我们可以发现,队列在线程池中是非常重要的角色,那么Executors就是根据不同的队列实现了功能不同的线程池,下面我们来看看 Executors包含的常用线程池1. 我们可以发现,coresize和maxsize相同,超时时间为0,队列用的LinkedBlockingQueue无界的FIFO队列,这表示什么,很明显,这个线程池始终只有 2. SynchronousQueue队列,一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作。所以,当我们提交第一个任务的时候,是加入不了队列的,这就满足了,一个线程池条件“当无法加入队列的时候,且任务没有达到maxsize时,我们将新开启一个线程任务”。所以我们的maxsize是big big。时间是60s,当一个线程没有任务执行会暂时保存60s超时时间,如果没有的新的任务的话,会从cache中remove掉。 3. 排队策略排队有三种通用策略:
使用直接提交策略,即SynchronousQueue。首先SynchronousQueue是无界的,也就是说他存数任务的能力是没有限制的,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加。在这里不是核心线程便是新创建的线程,但是我们试想一样下,下面的场景。 当核心线程已经有2个正在运行.
所以在使用SynchronousQueue通常要求maximumPoolSize是无界的,这样就可以避免上述情况发生(如果希望限制就直接使用有界队列)。对于使用SynchronousQueue的作用jdk中写的很清楚:此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。 使用无界队列策略,即LinkedBlockingQueue这个就拿newFixedThreadPool来说,根据前文提到的规则:如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。那么当任务继续增加,会发生什么呢? 有界队列,使用ArrayBlockingQueue。个是最为复杂的使用,所以JDK不推荐使用也有些道理。与上面的相比,最大的特点便是可以防止资源耗尽的情况发生。 假设,所有的任务都永远无法执行完。对于首先来的A,B来说直接运行,接下来,如果来了C,D,他们会被放到queu中,如果接下来再来E,F,则增加线程运行E,F。但是如果再来任务,队列无法再接受了,线程数也到达最大的限制了,所以就会使用拒绝策略来处理。 Summary
通常来说对于静态任务可以归为:
看完这篇问文章后,希望能够可以选择合适的类型。 ]]> 主要介绍 ThreadPool]]>最近在设计一个RPC框架,需要处理序列化的问题。有很多种序列化协议可以选择,比如Java原生的序列化协议,Protobuf, Thrift, Hessian, Kryo等等,这里说的序列化协议专指Java的基于二进制的协议,不是基于XML, JSON这种格式的协议。在实际开发中考虑了很多点,也遇到一些问题,拿出来说说。
性能性能包括两个方面,时间复杂度和空间复杂度。
经过上述,我们可以知道:序列化这件事说白了就是把一个对象变成一个二进制流,然后把二进制流再转化成对象的过程。前者好说,关键是后者,后者其实就是一个如何分帧(Frame)的问题,即从哪个字节开始读几个字节来还原成数据的问题。常见的分帧方式有:
需要考虑的问题对于Java序列化来说,肯定是第三种方式,但是如何设计这个分帧方式又有很多实现。下面说说上述具体有哪些考虑和问题。
那不能利用反射技术获得字段顺序,能不能利用字节码技术来获得这个类声明时存放的字段顺序呢?比如用ASM来直接读Class文件。但是我查阅了Java虚拟机规范,虚拟机规范只规定了Class文件中的元素,并没有要求实际存储的Filed[]按照声明顺序存储。这也是对的,实际的虚拟机实现可以按照各自的算法来优化。 解决方案
常见的应用是这样的 存在的问题Hessian, Kryo, Protobuf, Thrift在生成的字节数都有了优化,并且可以只发送部分设置了值的字段信息来完成序列化,这样节省的字节数就更多了。但是还有些问题:
总结不依赖中间文件来序列化并同时满足前3点,从上面的分析来看很难做到。Protobuf和Thrift这种使用IDL来生产中间文件的协议,除了从跨平台调用的角度的需要,也包含了序列化的需要。毕竟又要考虑跨语言,又想得到效率,明显是不可能的。只有通过牺牲我们自己的时间去创建IDL文件来达到我们的目的。 参考文章 ]]>最近在设计一个RPC框架,需要处理序列化的问题。有很多种序列化协议可以选择,比如Java原生的序列化协议,Protobuf, Thrift, Hessian, Kryo等等,这里说的序列化协议专指Java的基于二进制的协议,不是基于XM]]>
Monitor类是作为ReentrantLock的一个替代,代码中使用 Monitor比使用ReentrantLock更不易出错,可读性也更强,并且也没有显著的性能损失,使用Monitor甚至有潜在的性能得到优化。下面我们整体上对Monitor的源码结构做一下梳理,总的来说也就在从jdk最原生的wait、notify.再做了一层warp。提供更加丰富的API。比如,当我们要实现一个blockingQueue的时候,原生的代码大概是这样写的 上面的代码可能还不足以说明原生jdk中纯在的问题,但是原生的wait、notify无法做到更加精细的唤醒操作,而Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,就是多个监视器的意思。在不同的情况下使用不同的Condition。
如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒”读线程”时,不可能通过notify()或notifyAll()明确的指定唤醒”读线程”,而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。当所有的线程都被唤醒,这里会再次产生一个锁的竞争. 但是,通过Condition,就能明确的指定唤醒读线程。我们在编程的时候可以指定唤醒任何一个线程,如下 看吧有些事情是原生的wait、nofity而不能做到的,现在向大家展示Google guava库中的monitor。提供更多的API,更丰富的功能 提供更多的API
Future编程我们都知道jdk给了我们异步接口,叫做 这里我们会立即得到一个Future对象,但是我们的方法调用,并不能马上得到值,只有当我们调用Future#get()方法的时候,会导致一直阻塞到方法的值被得到,假如我们有3个RPC方法需要调用,RPC-1耗时3秒。RPC-2耗时2秒。RPC-1耗时1秒,如果我们通过传统的方法调用,就会耗时6s,必须等RPC-1调用完成之后,才能进行RPC-2,之后才能进行RPC-1. 我们只需要像下面这样做,就可以实现回调了 Futures类还提供了callback方法,可以得到future的返回值的回调方法 然们来看看callback的源码实现 先笔者说过了,如果我们要实现回调,还有一种方式就是,不断的去轮训future的计算状态是否是已完成。那么我们在getUninterruptibly方法里面看到了这个,虽然get会阻塞,但是getUninterruptibly是在callbackListener这个新的线程当中的
最后调用了future.addListener(callbackListener, executor);这个方法,最简单的回调,只是现在的callbackListener线程里面我们可以得到success和failure状态做一些事情。详情参加guava的源码吧 ]]>
同步(synchronous)和异步(asynchronous)的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起 IO 请求后需要等待或者轮询内核 IO 操作完成后才能继续执行;而异步是指用户线程发起 IO 请求后仍继续执行,当内核 IO 操作完成后会通知用户线程,或者调用用户线程注册的回调函数。 再说一下 IO 发生时涉及的对象和步骤。对于一个 network IO(这里我们以 read 举例),它会涉及到两个系统对象,一个是调用这个 IO 的 process(or thread),另一个就是系统内核(kernel)。当一个 read 操作发生时,它会经历两个阶段:
1. 同步阻塞 IO在 linux 中,默认情况下所有的 socket 都是 blocking,一个典型的读操作流程大概是这样: 几乎所有的程序员第一次接触到的网络编程都是从 listen()、send()、recv() 等接口开始的,这些接口都是阻塞型的。使用这些接口可以很方便的构建服务器 / 客户机的模型。下面是一个简单地”一问一答”的服务器。 我们注意到,大部分的 socket 接口都是阻塞型的。所谓阻塞型接口是指系统调用(一般是 IO 接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。 在上述的线程 / 时间图例中,主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供为前例同样的问答服务。 输入参数 s 是从 socket(),bind() 和 listen() 中沿用下来的 socket 句柄值。执行完 bind() 和 listen() 后,操作系统已经开始在指定的端口处监听所有的连接请求,如果有请求,则将该连接请求加入请求队列。调用 accept() 接口正是从 socket s 的请求队列抽取第一个连接信息,创建一个与 s 同类的新的 socket 返回句柄。新的 socket 句柄即是后续 read() 和 recv() 的输入参数。如果请求队列当前没有请求,则 accept() 将进入阻塞状态直到有请求进入队列。 2. 同步非阻塞 IOLinux 下,可以通过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操作时,流程是这个样子: 从图中可以看出,当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 error。从用户进程角度讲 ,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。 下面将给出只用一个线程,但能够同时从多个连接中检测数据是否送达,并且接受数据的模型。 在非阻塞状态下,recv() 接口在被调用后立即返回,返回值代表了不同的含义。如在本例中,
可以看到服务器线程可以通过循环调用 recv() 接口,可以在单个线程内实现对所有连接的数据接收工作。但是上述模型绝不被推荐。因为,循环调用 recv() 将大幅度推高 CPU 占用率;此外,在这个方案中 recv() 更多的是起到检测”操作是否完成”的作用,实际操作系统提供了更为高效的检测”操作是否完成”作用的接口,例如 select() 多路复用模式,可以一次检测多个连接是否活跃。 3. IO 多路复用
IO multiplexing 这个词可能有点陌生,但是如果我说 select / epoll,大概就都能明白了。有些地方也称这种 IO 方式为事件驱动 IO(event driven IO)。我们都知道,select / epoll 的好处就在于单个 process 就可以同时处理多个网络连接的 IO。它的基本原理就是 select / epoll 这个 function 会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。它的流程如图: 当用户进程调用了 select,那么整个进程会被 block,而同时,kernel 会”监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。 其中 while 循环前将 socket 添加到 select 监视中,然后在 while 内一直调用 select 获取被激活的 socket,一旦 socket 可读,便调用 read 函数将 socket 中的数据读取出来。 如图所示,EventHandler 抽象类表示 IO 事件处理器,它拥有 IO 文件句柄 Handle(通过 get_handle 获取),以及对 Handle 的操作 handle_event(读/写等)。继承于 EventHandler 的子类可以对事件处理器的行为进行定制。Reactor 类用于管理 EventHandler(注册、删除等),并使用 handle_events 实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数 select,只要某个文件句柄被激活(可读/写等),select 就返回(阻塞),handle_events 就会调用与文件句柄关联的事件处理器的 handle_event 进行相关操作 如图所示,通过 Reactor的方式,可以将用户线程轮询 IO 操作状态的工作统一交给 handle_events 事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而 Reactor 线程负责调用内核的 select 函数检查 socket 状态。当有 socket 被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行 handle_event 进行数据读取、处理的工作。由于 select 函数是阻塞的,因此多路 IO 复用模型也被称为异步阻塞 IO 模型。注意,这里的所说的阻塞是指 select 函数执行时线程被阻塞,而不是指 socket。一般在使用 IO 多路复用模型时,socket 都是设置为 NONBLOCK 的,不过这并不会产生影响,因为用户发起 IO 请求时,数据已经到达了,用户线程一定不会被阻塞。 用户需要重写 EventHandler 的 handle_event 函数进行读取数据、处理数据的工作,用户线程只需要将自己的 EventHandler 注册到 Reactor 即可。Reactor 中 handle_events 事件循环的伪代码大致如下。 事件循环不断地调用 select 获取被激活的 socket,然后根据获取 socket 对应的 EventHandler,执行器 handle_event 函数即可。 这里,fd_set 类型可以简单的理解为按 bit 位标记句柄的队列,例如要在某 fd_set 中标记一个值为16的句柄,则该 fd_set 的第16个 bit 位被标记为1。具体的置位、验证可使用 FD_SET、FD_ISSET 等宏实现。在 select() 函数中,readfds、writefds 和 exceptfds 同时作为输入参数和输出参数。如果输入的 readfds 标记了16号句柄,则 select() 将检测16号句柄是否可读。在 select() 返回后,可以通过检查 readfds 有否标记16号句柄,来判断该”可读”事件是否发生。另外,用户可以设置 timeout 时间。 下面将重新模拟上例中从多个客户端接收数据的模型。 上述模型只是描述了使用 select() 接口同时从多个客户端接收数据的过程;由于 select() 接口可以同时对多个句柄进行读状态、写状态和错误状态的探测,所以可以很容易构建为多个客户端提供独立问答服务的服务器系统。如下图。 这里需要指出的是,客户端的一个 connect() 操作,将在服务器端激发一个”可读事件”,所以 select() 也能探测来自客户端的 connect() 行为。 这种模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。我们可以将这种模型归类为”事件驱动模型”。 幸运的是,有很多高效的事件驱动库可以屏蔽上述的困难,常见的事件驱动库有 libevent 库,还有作为 libevent 替代者的 libev 库。这些库会根据操作系统的特点选择最合适的事件探测接口,并且加入了信号(signal)等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择。 4. 异步IOLinux 下的 asynchronous IO 其实用得不多,从内核2.6版本才开始引入。先看一下它的流程: 用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从 kernel 的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。然后,kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel 会给用户进程发送一个 signal,告诉它 read 操作完成了。 如图所示,异步 IO 模型中,用户线程直接使用内核提供的异步 IO API 发起 read 请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的 AsynchronousOperation 和 CompletionHandler 注册到内核,然后操作系统开启独立的内核线程去处理 IO 操作。当 read 请求的数据到达时,由内核负责读取 socket 中的数据,并写入用户指定的缓冲区中。最后内核将 read 的数据和用户线程注册的 CompletionHandler 分发给内部 Proactor,Proactor 将 IO 完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步 IO。 用户需要重写 CompletionHandler 的 handle_event 函数进行处理数据的工作,参数 buffer 表示 Proactor 已经准备好的数据,用户线程直接调用内核提供的异步 IO API,并将重写的 CompletionHandler 注册即可。
经过上面的介绍,会发现 non-blocking IO 和 asynchronous IO 的区别还是很明显的。在 non-blocking IO 中,虽然进程大部分时间都不会被 block,但是它仍然要求进程去主动的 check,并且当数据准备完成以后,也需要进程主动的再次调用 recvfrom 来将数据拷贝到用户内存。而 asynchronous IO 则完全不同。它就像是用户进程将整个 IO 操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查 IO 操作的状态,也不需要主动的去拷贝数据。 ]]>
什么是锁锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂。因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字( 一个简单的锁让我们从java中的一个同步块开始: 可以看到在inc()方法中有一个synchronized(this)代码块。该代码块可以保证在同一时间只有一个线程可以执行return ++count。虽然在synchronized的同步块中的代码可以更加复杂,但是++count这种简单的操作已经足以表达出线程同步的意思。 以下的Counter类用Lock代替synchronized达到了同样的目的: lock()方法会对Lock实例对象进行加锁,因此所有对该对象调用lock()方法的线程都会被阻塞,直到该Lock对象的unlock()方法被调用。 注意其中的while(isLocked)循环,它又被叫做“自旋锁”。自旋锁以及wait()和notify()方法在线程通信这篇文章中有更加详细的介绍。当isLocked为true时,调用lock()的线程在wait()调用上阻塞等待。为防止该线程没有收到notify()调用也从wait()中返回(也称作虚假唤醒),这个线程会重新去检查isLocked条件以决定当前是否可以安全地继续执行还是需要重新保持等待,而不是认为线程被唤醒了就可以安全地继续执行了。如果isLocked为false,当前线程会退出while(isLocked)循环,并将isLocked设回true,让其它正在调用lock()方法的线程能够在Lock实例上加锁。 当线程完成了临界区(位于lock()和unlock()之间)中的代码,就会调用unlock()。执行unlock()会重新将isLocked设置为false,并且通知(唤醒)其中一个(若有的话)在lock()方法中调用了wait()函数而处于等待状态的线程。 锁的可重入性Java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另一个java代码块。下面是一个例子: 调用inner()就没有什么问题,因为这两个方法(代码块)都由同一个管程对象(”this”)所同步。如果一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。 前面给出的锁实现不是可重入的。如果我们像下面这样重写Reentrant类,当线程调用outer()时,会在inner()方法的lock.lock()处阻塞住。 调用outer()的线程首先会锁住Lock实例,然后继续调用inner()。inner()方法中该线程将再一次尝试锁住Lock实例,结果该动作会失败(也就是说该线程会被阻塞),因为这个Lock实例已经在outer()方法中被锁住了。 两次lock()之间没有调用unlock(),第二次调用lock就会阻塞,看过lock()实现后,会发现原因很明显: 一个线程是否被允许退出lock()方法是由while循环(自旋锁)中的条件决定的。当前的判断条件是只有当isLocked为false时lock操作才被允许,而没有考虑是哪个线程锁住了它。 为了让这个Lock类具有可重入性,我们需要对它做一点小的改动: 注意到现在的while循环(自旋锁)也考虑到了已锁住该Lock实例的线程。如果当前的锁对象没有被加锁(isLocked = false),或者当前调用线程已经对该Lock实例加了锁,那么while循环就不会被执行,调用lock()的线程就可以退出该方法(译者注:“被允许退出该方法”在当前语义下就是指不会调用wait()而导致阻塞)。 除此之外,我们需要记录同一个线程重复对一个锁对象加锁的次数。否则,一次unblock()调用就会解除整个锁,即使当前锁已经被加锁过多次。在unlock()调用没有达到对应lock()调用的次数之前,我们不希望锁被解除。 现在这个Lock类就是可重入的了。 锁的公平性Java的synchronized块并不保证尝试进入它们的线程的顺序。因此,如果多个线程不断竞争访问相同的synchronized同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权总是分配给了其它线程。这种情况被称作线程饥饿。为了避免这种问题,锁需要实现公平性。本文所展现的锁在内部是用synchronized同步块实现的,因此它们也不保证公平性。饥饿和公平中有更多关于该内容的讨论。 这个简单的结构可以保证当临界区抛出异常时Lock对象可以被解锁。如果不是在finally语句中调用的unlock(),当临界区抛出异常时,Lock对象将永远停留在被锁住的状态,这会导致其它所有在该Lock对象上调用lock()的线程一直阻塞 ]]> 什么是锁锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchroniz]]> 已废弃的Thread.stop()
如上是Hotspot JDK 7中的java.lang.Thread.stop()的代码,学习一下它的doc:
是不是差点被这段话绕晕,俗点说:目标线程可能持有一个监视器,假设这个监视器控制着某两个值之间的逻辑关系,如var1必须小于var2,某一时刻var1等于var2,本来应该受保护的逻辑关系,不幸的是此时恰好收到一个stop命令,产生一个ThreadDeath错误,监视器被解锁。这就导致逻辑错误,当然这种情况也可能不会发生,是不可预料的。注意:ThreadDeath是何方神圣?是个java.lang.Error,不是java.lang.Exception。
其实这里已经暗示停止一个线程的最佳方法:条件变量 或 条件变量+中断。
其它关于stop方法的doc:
小结:Thread.stop()不安全,已不再建议使用。 令人迷惑的thread.interrupt()Thread类中有三个方法会令新手迷惑,他们是: 如果按照近几年流行的重构,代码整洁之道,程序员修炼之道等书的观点,这几个方法的命名相对于其实现的功能来说,不够直观明确,极易令人混淆,是低级程序猿的代码。逐个分析: 中断本线程。无返回值。具体作用分以下几种情况:
中断一个不处于活动状态的线程不会有任何作用。如果是其他线程在中断该线程,则java.lang.Thread.checkAccess()方法就会被调用,这可能抛出java.lang.SecurityException。
检测当前线程是否已经中断,是则返回true,否则false,并清除中断状态。换言之,如果该方法被连续调用两次,第二次必将返回false,除非在第一次与第二次的瞬间线程再次被中断。如果中断调用时线程已经不处于活动状态,则返回false。 检测当前线程是否已经中断,是则返回true,否则false。中断状态不受该方法的影响。如果中断调用时线程已经不处于活动状态,则返回false。
在hotspot源码中,两者均通过调用的native方法isInterrupted(boolean)来实现,区别是参数值ClearInterrupted不同。 经过上面的分析,三者之间的区别已经很明确,来看一个具体案例,是我在工作中看到某位架构师的代码,只给出最简单的概要结构: 我最初被这段代码直接绕晕,用thread.isInterrupted()方法作为循环中止条件可以吗? 根据上文的分析,当该方法阻塞于wait/join/sleep时,中断状态会被清除掉,同时收到InterruptedException,也就是接收到的值为false。上述代码中,当sleep之后的调用otherDomain.xxx(),otherDomain中的代码包含wait/join/sleep并且InterruptedException被catch掉的时候,线程无法正确的中断。 因此,在编写多线程代码的时候,任何时候捕获到InterruptedException,要么继续上抛,要么重置中断状态,这是最安全的做法,参考『Java Concurrency in Practice』。凡事没有绝对,如果你可以确保一定没有这种情况发生,这个代码也是可以的。
当某个方法抛出InterruptedException时,表示该方法是一个阻塞方法。当在代码中调用一个将抛出InterruptedException异常的方法时,你自己的方法也就变成了一个阻塞方法,并且必须要处理对中断的相应。对于库代码来说,有两种选择:
最后再强调一遍,②处的 Thread.currentThread().interrupt() 非常非常重要 最佳实践:Shared Variable不记得哪本书上曾曰过,最佳实践是个烂词。在这里这个词最能表达意思,停止一个线程最好的做法就是利用共享的条件变量。 对于本问题,我认为准确的说法是:停止一个线程的最佳方法是让它执行完毕,没有办法立即停止一个线程,但你可以控制何时或什么条件下让他执行完毕 通过条件变量控制线程的执行,线程内部检查变量状态,外部改变变量值可控制停止执行。为保证线程间的即时通信,需要使用使用volatile关键字或锁,确保读线程与写线程间变量状态一致。下面给一个最佳模板: 当④处的代码阻塞于wait()或sleep()时,线程不能立刻检测到条件变量。因此②处的代码最好同时调用interrupt()方法。 小结:
总结:
总之,中断只是一种协作机制,需要被中断的线程自己处理中断。停止一个线程最佳实践是 中断 + 条件变量。 参考文献]]>已废弃的Thread.stop()在GoF的23种设计模式中,单例模式是比较简单的一种。然而,有时候越是简单的东西越容易出现问题。下面就单例设计模式详细的探讨一下。 1. 最简单的实现首先,能够想到的最简单的实现是,把类的构造函数写成private的,从而保证别的类不能实例化此类,然后在类中提供一个静态的实例并能够返回给使用者。这样,使用者就可以通过这个引用使用到这个类的实例了。 如上例,外部使用者如果需要使用SingletonClass的实例,只能通过getInstance()方法,并且它的构造方法是private的,这样就保证了只能有一个对象存在。 2. 性能优化——lazy loaded上面的代码虽然简单,但是有一个问题——无论这个类是否被使用,都会创建一个instance对象。如果这个创建过程很耗时,比如需要连接10000次数据库(夸张了…:-)),并且这个类还并不一定会被使用,那么这个创建过程就是无用的。怎么办呢? 代码的变化有两处——首先,把instance初始化为null,直到第一次使用的时候通过判断是否为null来创建对象。因为创建过程不在声明处,所以那个final的修饰必须去掉。 我们来想象一下这个过程。要使用SingletonClass,调用getInstance()方法。第一次的时候发现instance是null,然后就新建一个对象,返回出去;第二次再使用的时候,因为这个instance是static的,所以已经不是null了,因此不会再创建对象,直接将其返回。 3. 同步上面的代码很清楚,也很简单。然而就像那句名言:“80%的错误都是由20%代码优化引起的”。单线程下,这段代码没有什么问题,可是如果是多线程,麻烦就来了。我们来分析一下: 线程A希望使用SingletonClass,调用getInstance()方法。因为是第一次调用,A就发现instance是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用SingletonClass,调用getInstance()方法,同样检测到instance是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个SingletonClass的对象——单例失败! 解决的方法也很简单,那就是加锁: 是要getInstance()加上同步锁,一个线程必须等待另外一个线程创建完成后才能使用这个方法,这就保证了单例的唯一性。 4. 又是性能上面的代码又是很清楚很简单的,然而,简单的东西往往不够理想。这段代码毫无疑问存在性能的问题——synchronized修饰的同步块可是要比一般的代码段慢上几倍的!如果存在很多次getInstance()的调用,那性能问题就不得不考虑了! 让我们来分析一下,究竟是整个方法都必须加锁,还是仅仅其中某一句加锁就足够了?我们为什么要加锁呢?分析一下出现lazy loaded的那种情形的原因。原因就是检测null的操作和创建对象的操作分离了。如果这两个操作能够原子地进行,那么单例就已经保证了。于是,我们开始修改代码: 首先去掉getInstance()的同步操作,然后把同步锁加载if语句上。但是这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要同步,性能问题还是存在。如果……如果我们事先判断一下是不是为null再去同步呢? 还有问题吗?首先判断instance是不是为null,如果为null,加锁初始化;如果不为null,直接返回instance。这就是double-checked locking设计实现单例模式。到此为止,一切都很完美。我们用一种很聪明的方式实现了单例模式。 5. 从源头检查代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。 要知道,JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。 下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。 下面我们来考虑这么一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了! 于是,我们想到了下面的代码: 我们在第一个同步块里面创建一个临时变量,然后使用这个临时变量进行对象的创建,并且在最后把instance指针临时变量的内存空间。写出这种代码基于以下思想,即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此,在外部的同步块里面对临时变量sc进行操作并不影响instance,所以外部类在instance=sc;之前检测instance的时候,结果instance依然是null。 不过,这种想法完全是错误的!同步块的释放保证在此之前——也就是同步块里面——的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把instance=sc;这句移到内部同步块里面执行。这样,程序又是错误的了! 6. 解决方案说了这么多,难道单例没有办法在Java中实现吗?其实不然! 然而,这只是JDK1.5之后的Java的解决方案,那之前版本呢?其实,还有另外的一种解决方案,并不会受到Java版本的影响: 在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。 由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。 至此,我们完整的了解了单例模式在Java语言中的时候,提出了两种解决方案。个人偏向于第二种,并且Effiective Java也推荐的这种方式。 在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。 由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。 至此,我们完整的了解了单例模式在Java语言中的时候,提出了两种解决方案。个人偏向于第二种,并且Effiective Java也推荐的这种方式。 在GoF的23种设计模式中,单例模式是比较简单的一种。然而,有时候越是简单的东西越容易出现问题。下面就单例设计模式详细的探讨一下。 下面是我写给学弟们在学习路线上的一些建议,大家可以参考一下!希望能够帮助到大家,有写得不太合适的地方,还请大家指正! 一、基础
我看过的书籍,留个参考!(当然没有全部掌握)
36氪:wow36kr
硅发布:guifabucom 虎嗅网:huxiu_com
InfoQ:infoqchina
中文互联网数据研究资讯中心:i199it
伯乐在线:jobbole
深蓝阅读:bluereader
互联网er的早读课
程序员:imkuqin
程序人生:programmer_life
程序员那些事:iProgrammer
姑婆那些事儿:gupo520
数据库开发:DBDevs
CPP开发者:cppFans
Python开发者:PythonCoder
ImportNew:importnew
Linux爱好者:LinuxHub
Linux中国:linux-cn
Linux编程(添加朋友 → 公众号 → 搜索“Linux编程”,第一个黑色头像的就是)
前端大全:FrontDev
安卓应用开发:AndroidPD
iOS大全:iOShub
PHP开发者:PHPDevs
DotNet:iDotNet
设计的那些事:aboutDesigner
网页设计精选:BestWebDesign
UI设计达人:BestUIDesign
机器之心:almosthuman2014
统计之都:CapStat
数据挖掘:datadw
数据挖掘菜鸟:data_bird
大数据文摘:BigDataDigest
可视化之美:infovis
数盟:DataScienceUnion
数据挖掘与数据分析:datakong
大数据实验室:bigdatalab
SOTON数据分析:soton2014sky
数据派:datapi
大数据邦:bigdatabang
R语言:Ryuyan360
R语言中文网:rchinanet
R语言论坛:Ryuyanluntan
待字闺中:daiziguizhongren
Crossin的编程教室:crossincode
阿里研究院:aliresearch
阿里商业评论:Alibusinessreview
百度营销研究院
腾讯研究院:cyberlawrc
道哥的黑板报:taoasay
二爷鉴书:findbook
IT鉴书
人邮IT书坊:ptpressitbooks
图灵教育:turingbooks
LinkedIn中国:LinkedIn-China
肉饼铺子:robbinthoughts
投资人子柳:vc-ziliu
小道消息:WebNotes
移动观察:mobileweb
青龙老贼:Z_talk
懒人在思考:lazy-thought
TimYang:timyang_net
CSDN:CSDNnews
CSDN云计算:CSDNcloud
CSDN大数据:csdnbigdata
developerWorks:developerWorks dockerpool:dockerpool
慕课网:imooc815
慕课网imooc:imooc-com
MOOC学院:GuokrMOOC
贴两篇我在知乎上面关于这方面的回答最后送大家一些话]]> 下面是我写给学弟们在学习路线上的一些建议,大家可以参考一下!希望能够帮助到大家,有写得不太合适的地方,还请大家指正! 一、基础 |
---|