#
作者 张龙 发布于 2010年8月27日 上午12时59分
- .NET,
- Java
- 主题
- 性能和可伸缩性
- 标签
- 性能和扩展性 ,
- 性能评估 ,
- 性能调优
今年5月底,瑞士计算机世界杂志上刊登了Web性能诊断专家Bernd Greifeneder的一篇文章,文章列举了其在过去几年工作中所遇到的服务器端编程的十大性能问题。Andreas Grabner则在自己的博客上对这些性能问题给出了进一步阅读的链接。希望这些问题与相关的延伸阅读能为广大的InfoQ读者带来一定的启示。
问题一:过多的数据库调用
我们发现经常出现的一个问题就是在每次请求/事务中存在过多的数据库查询。有如下三个场景作为佐证:
- 在一次事务上下文中所请求的数据比实际需要的数据多出很多。比如说:请求所有的账户信息而不是仅仅查询出当前需要显示的信息。
- 多次请求同样的数据。这种情况通常发生在相同事务中的不同组件之间是彼此独立的,而每个组件都会请求同样的数据。我们并不清楚当前上下文中已经加载了哪些数据,最后只得多次发出同样的查询。
- 发出多个查询语句以获得某一数据集。通常这是由于没有充分利用到复杂的SQL语句、存储过程等在一次批处理中获取需要的数据所导致的。
延伸阅读:Blog on Linq2Sql Performance Issues on Database、Video on Performance Anti-Patterns
问题二:过多地使用同步
毫无疑问,同步对于应用中共享数据的保护来说是至关重要的举措。但有很多开发者却过度使用同步,比如在超大段的代码中使用同步。在低负载的情况下,这么做倒没什么问题;但在高负载或是产品环境下,过度的同步会导致严重的性能与可伸缩性问题。
延伸阅读: How to identify synchronization problems under load
问题三:过度使用远程调用
很多库都使用了远程通信。对于开发者来说,远程调用与本地调用似乎没什么区别,但如果不清楚远程调用的本质就会铸成大错,因为每一次远程调用都会涉及到延迟、序列化、网络堵塞以及内存使用等问题。如果没有经过深思熟虑而盲目使用这些远程技术就会导致严重的性能与可伸缩性问题。
延伸阅读: Performance Considerations in Distributed Applications
问题四:错误地使用对象关系映射
对象关系映射为开发者解决了很多负担,比如从数据库中加载对象以及将对象持久化到数据库中。但与其他任何框架一样,对象关系映射也有很多配置选项需要优化,只有这样才能适应于当前应用的需要。错误的配置与不正确的使用都会导致始料不及的性能问题。在使用对象关系映射框架前,请务必保证熟悉所有的配置,如果有机会,请深入到所用框架的内核,这样使用起来才有保障。
延伸阅读:Understanding Hibernate Session Cache、Understanding the Query Cache、Understanding the Second Level Cache
问题五:内存泄漏
托管的运行时环境(如Java和.NET)可以通过垃圾收集器进行内存管理。但垃圾收集器无法避免内存泄漏问题。“被遗忘”的对象依旧会占据着内存,最终将会导致内存泄漏问题。当对象不再需要时,请尽快释放掉对象引用。
延伸阅读:Understanding and finding Memory Leaks
问题六:使用有问题的第三方代码/组件
没有人会从头编写应用的全部功能。我们都会使用第三方程序库来加快开发进程。这么做不仅会加速产出,也增加了性能上的风险。虽然大多数框架都具有良好的文档并且经过了充分的测试,但没人能够保证这些框架在任何时候都会像预期的那样好。因此,在使用这些第三方框架时,事先一定要做好充分的调研。
延伸阅读: Top SharePoint Performance Mistakes
问题七:对稀缺资源的使用存在浪费的情况
内存、CPU、I/O以及数据库等资源属于稀缺资源。在使用这些资源时如果存在浪费的情况就会造成严重的性能与可伸缩性问题。比如说,有人会长时间打开数据库连接而不关闭。连接应该只在需要的时候才使用,使用完毕后就应该放回到连接池中。我们经常看到有人在请求一开始就去获取连接,直到最后才释放,这么做会导致性能瓶颈。
延伸阅读: Resource Leak detection in .NET Applications
问题八:膨胀的Web前端
由于现在的Web速度越来越快,用户的网络体验也越来越好。在这个趋势下,很多应用的前端都提供了太多的内容,但这么做会导致差劲的浏览体验。很多图片都太大了,没有利用好或是错误地使用了浏览器缓存、过度地使用JavaScript/AJAX等,所有这一切都会导致浏览器的性能问题。
延伸阅读: How Better Caching would help speed up Frankfurt Airport Web Site、Best Practices on Web Performance Optimization
问题九:错误的缓存策略导致过度的垃圾收集
将对象缓存在内存中可以避免每次都向数据库发出请求,这么做可以提升性能。但如果缓存了太多的对象,或是缓存了很多不常使用的对象则会将缓存的这种优势变成劣势,因为这会导致过高的内存使用率及过多的垃圾收集活动。在实现缓存策略前,请想好哪些对象需要缓存,哪些对象不需要缓存,进而避免这类性能与可伸缩性问题。
延伸阅读:Java Memory Problems、Identify GC Bottlenecks in Distributed Applications
问题十:间歇性问题
间歇性问题很难发现。通常这类问题与特定的输入参数有关,或是发生在某个负载条件下。完全的测试覆盖率及负载与性能测试能在这些问题产生前就发现他们。
延伸阅读:Tracing Intermittent Errors by Lucy Monahan from Novell、How to find invisible performance problems
最近因为解析socket 于是就遇到二进制这些东西,在学校没学好而且以前不是很理解,所有重新开始温故了一些基本概念,
首先是进制的概念,所谓的进制就是数学计算的具体多少而进位的一种算法。比如二进制,就只有0和1 他们基本是到2就进位。
而现实生活中也有各种进位方式,比如常用的十进制,我们基本货币计算就是这种方式,因此还有八进位,十六进位等等,
下面我把这些进位对应的英文也列出来,以为在编程的时候 常常看到的命名是相关英文而非中文,理解这样英文便于你的具体
应用或者查看别人API。
十进制数(Decimal)
二进制数(Binary)
七进制数(septenary)
八进制数(Octal)
十六进制数(Hex)
六十进位制数(Sixty binary)
其实本身这些进制都是机器可读的语言,对应同样的东西 他们只是表达的方式不一样,表达的都是同一个东西,
那么本身进制直接可能通过操作相互转化,这个转化就比较枯燥,一般语言都提供API来封装了这个转化过程。
进制数我刚才说了,我理解为机器可读的标识,那么对应人的话,一般我们看到的都是图形化的东西,因此最早老美提出了
ASCII,因为是老美提出来的,所以他只讲他们的语言的基本元素A B C D...
ASCII里面分显示字符和控制字符,一般控制字符不能显示在页面。
具体可以参考
http://zh.wikipedia.org/zh/ASCII
随着全球化的进程,ASCII太局限了,因此Unicode更为普及。
理解了基本原理: 我们调用apache Codec 的api 来看看
org.apache.commons.codec.binary
Class BinaryCodec:
Translates between byte arrays and strings of "0"s and "1"s.
例子
String s ="00011111";
BinaryCodec bc = new BinaryCodec();
byte[] b = bc.toByteArray(s); //b 调试结果为 [31] 其实就是acii 上面描述的十进制表示
String t = new String(b); //t 不能看到 因为这是控制字符
System.out.println(t);
如果
String s ="00100001";
BinaryCodec bc = new BinaryCodec();
byte[] b = bc.toByteArray(s); //b 调试结果为 [33] 其实就是acii 上面描述的十进制表示
String t = new String(b); //t 能看到 是字符!
System.out.println(t);
而这个s 必须是1 0表示的二进制。toByteArray这个应该表示将二进制显示的字符串转化为真正的显示意义上的二进制。
String s ="00100001";
BinaryCodec bc = new BinaryCodec();
byte[] b = bc.toByteArray(s); //b=[33]
char[] d = bc.toAsciiChars(b); //d= [0, 0, 1, 0, 0, 0, 0, 1]
String str = bc.toAsciiString(b);//str = 00100001
byte[] e = bc.toAsciiBytes(b); //e = [48, 48, 49, 48, 48, 48, 48, 49]
BinaryCodec bc = new BinaryCodec();
char[] c = {'0','0','1','0','0','0','0','1'};
byte[] b = bc.fromAscii(c); //[33]
如果
char[] c = {'0','0','1','J','0','0','0','1'}; //表示二进制
还是//[33] 这说明除了1所有的char 在该API都认为是0,来处理二进制
如果是
char[] c = {'0','0','1','J','0','0','0'}; //七位
那么b 就为空[]
如果是九位
char[] c = {'0','0','1','J','0','0','0','0'}; //九位
如果最后一位为1 那么结果 b=[65]
如果为非1 比如0 或者其他char 那么结果b=[64]
方法
static byte[] fromAscii(byte[] ascii) 和上面的
static byte[] fromAscii(char[] ascii) 一样
比如
byte[] i = {'0','0','1','J','0','0','0','0','l'};
byte[] b = bc.fromAscii(i); //b的结果仍然是b=[64]
但是 在调试看到i 显示为 i=[48, 48, 49, 74, 48, 48, 48, 48, 108]
////////////////解码
byte[] decode(byte[] ascii) 将1,0 表示的byte数组解码为相应的byte
比如:
BinaryCodec bc = new BinaryCodec();
byte[] i = {'0','0','1','J','0','0','0','0','l'}; //i=[48, 48, 49, 74, 48, 48, 48, 48, 108]
byte[] b = bc.decode(i); //b=[64]
再比如:Object decode(Object ascii) :
String s ="00011111";
BinaryCodec bc = new BinaryCodec();
try {
Object t =bc.decode(s); //t=[31]
System.out.println(t);
} catch (DecoderException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
/////////////////编码
将byte转化为1,0 表示的byte数组
byte[] encode(byte[] raw)
Object encode(Object raw)
例如:
String s ="00011111";
BinaryCodec bc = new BinaryCodec();
try {
Object t =bc.encode(s);
System.out.println(t);
} catch (EncoderException e) {
// TODO Auto-generated catch block
e.printStackTrace(); //会抛出e:org.apache.commons.codec.EncoderException: argument not a byte array
}
如果
byte[] i = {'!'};
BinaryCodec bc = new BinaryCodec();
Object t =bc.encode(i); //t=[48, 48, 49, 48, 48, 48, 48, 49]
System.out.println(t);
事件管道模型
很多人知道事件驱动模式,那么事件管道(Event Pipeline)模式是什么?下面我们一起来探讨
event 的出现是设计为一个对象的状态的改变即就通知对该对象感兴趣的其他对象。
一般产生一个event总是有个发源地,我们可以把这个发源地叫做Event Generators而在java的EventObject interface中其实应该就是指的
source参数。
通常我们事件驱动模式是在一个本机上做,其实这个模式在分布式环境中也可以采用事件驱动模式。从整个模式上看,本地的事件驱动模型(local event)
和远程的事件驱动模型(remote event)没有什么本质区别,但有几点需要注意
1.local event:所有的对象都在本地包括需要通知的对象。
remote event:Event Generators在一段,remote event 有Event Generators产生后通知另外远端的entity。
2.因为有远端那么就涉及到网络通信,因此通常不能保证网络通信一直通畅,所有remote event的模式可能或出现event丢失不能达到对方的情况。
3.local event 在本地,所以往往event很快,相应的处理的反馈也快,因此你会发现本地的event模式通常都类似于http的握手模式,也就是一个event触发
通常就直接通知给所有监听器。
而remote event 是远程event的传送,那么在网络通信其实消耗大量的时间,那么我认为client端多event的发送,server端统一处理一批event可能是节省资源
比较好的方式。
因此我们这里引入Event Pipeline,他是对server端接收到的event进行管理,将event 放入管道进行各自策略的处理。
另外我们把远端的Event Listener叫做Remote Event Listener,其实本质是一样的,只是为了区分。
当许多remote events 在同一个管道的时候,那么我们需要根据不同的需要来定制event的策略,下面列举以下一些策略
a.In-order delivery
events按照某种顺序传递
b. Efficient delivery
多个event 合并为一个event
c. Store and forward
event 暂停在管道中,等待某个条件出现再继续传递
d. Filtering of events
根据条件过滤相应event
e. Grouping of events
多个event 被一个event代替
PipeLine 流程:
Event Generator ---> Event pipeLine -->Event Consumer
其中pipeLine中可以动态地设置一个或者多个策略。
1.Channel
channel 是负责数据读,写的对象,有点类似于老的io里面的stream,他和stream的区别,channel是双向的
既可以write 也可以read,而stream要分outstream和inputstream。而且在NIO中用户不应该直接从channel中读写数据,
而是应该通过buffer,通过buffer再将数据读写到channel中。
一个channel 可以提供给用户下面几个信息
(1)channel的当前状态,比如open 还是closed
(2)ChannelConfig对象,表示channel的一些参数,比如bufferSize
(3)channel支持的所有i/o操作(比如read,write,connect.bind)以及ChannelPipeLine(下面解释)
2.ChannelConfig
channel的参数,以Map 数据结构来存储
3.ChannelEvent
ChannelEvent广义的认为Channel相关的事件,他是否分Upstream events和downstream events两大块,这里需要注意的,让是server为
主体的话,从client的数据到server的过程是Upstream;而server到client的数据传输过程叫downstream;而如果以client为主体
的话,从server到client的过程对client来说是Upstream,而client到server的过程对client来说就是downstream。
Upstream events包括:
messageReceived:信息被接受时 ---MessageEvent
exceptionCaught:产生异常时 ---ExceptionEvent
channelOpen:channel被开启时 ---ChannelStateEvent
channelClosed:channel被关闭时 ---ChannelStateEvent
channelBound:channel被开启并准备去连接但还未连接上的时候 ---ChannelStateEvent
channelUnbound:channel被开启不准备去连接时候 ---ChannelStateEvent
channelConnected:channel被连接上的时候 ---ChannelStateEvent
channelDisconnected:channel连接断开的时候 ---ChannelStateEvent
channelInterestChanged:Channel的interestOps被改变的时候 ------ChannelStateEvent
writeComplete:写到远程端完成的时候 --WriteCompletionEvent
Downstream events包括:
write:发送信息给channel的时候 --MessageEvent
bind:绑定一个channel到指定的本地地址 --ChannelStateEvent
unbind:解除当前本地端口的绑定--ChannelStateEvent
connect:将channel连接到远程的机 --ChannelStateEvent
disconnect:将channel与远程的机连接断开 --ChannelStateEvent
close:关闭channel --ChannelStateEvent
需要注意的是,这里没有open event,这是因为当一个channel被channelFactory创建的话,channel总是已经被打开了。
此外还有两个事件类型是当父channel存在子channel的情况
childChannelOpen:子channel被打开 ---ChannelStateEvent
childChannelClosed:子channel被关闭 ---ChannelStateEvent
4.ChannelHandler
channel是负责传送数据的载体,那么数据肯定需要根据要求进行加工处理,那么这个时候就用到ChannelHandler
不同的加工可以构建不同的ChannelHandler,然后放入ChannelPipeline中
此外需要有ChannelEvent触发后才能到达ChannelHandler,因此根据event不同有下面两种的sub接口ChannelUpstreamHandler
和ChannelDownstreamHandler。
一个ChannelHandler通常需要存储一些状态信息作为判断信息,常用做法定义一个变量
比如
public class DataServerHandler extends {@link SimpleChannelHandler} {
*
* <b>private boolean loggedIn;</b>
*
* {@code @Override}
* public void messageReceived({@link ChannelHandlerContext} ctx, {@link MessageEvent} e) {
* {@link Channel} ch = e.getChannel();
* Object o = e.getMessage();
* if (o instanceof LoginMessage) {
* authenticate((LoginMessage) o);
* <b>loggedIn = true;</b>
* } else (o instanceof GetDataMessage) {
* if (<b>loggedIn</b>) {
* ch.write(fetchSecret((GetDataMessage) o));
* } else {
* fail();
* }
* }
* }
* ...
* }
// Create a new handler instance per channel.
* // See {@link Bootstrap#setPipelineFactory(ChannelPipelineFactory)}.
* public class DataServerPipelineFactory implements {@link ChannelPipelineFactory} {
* public {@link ChannelPipeline} getPipeline() {
* return {@link Channels}.pipeline(<b>new DataServerHandler()</b>);
* }
* }
除了这种,每个ChannelHandler都可以从ChannelHandlerContext中获取或设置数据,那么下面的做法就是利用ChannelHandlerContext
设置变量
* {@code @Sharable}
* public class DataServerHandler extends {@link SimpleChannelHandler} {
*
* {@code @Override}
* public void messageReceived({@link ChannelHandlerContext} ctx, {@link MessageEvent} e) {
* {@link Channel} ch = e.getChannel();
* Object o = e.getMessage();
* if (o instanceof LoginMessage) {
* authenticate((LoginMessage) o);
* <b>ctx.setAttachment(true)</b>;
* } else (o instanceof GetDataMessage) {
* if (<b>Boolean.TRUE.equals(ctx.getAttachment())</b>) {
* ch.write(fetchSecret((GetDataMessage) o));
* } else {
* fail();
* }
* }
* }
* ...
* }
* public class DataServerPipelineFactory implements {@link ChannelPipelineFactory} {
*
* private static final DataServerHandler <b>SHARED</b> = new DataServerHandler();
*
* public {@link ChannelPipeline} getPipeline() {
* return {@link Channels}.pipeline(<b>SHARED</b>);
* }
* }
这两种做法还是有区别的,上面的变量做法,每个new的handler 对象,变量是不共享的,而下面的ChannelHandlerContext是共享的
如果需要不同的handler之间共享数据,那怎么办,那就用ChannelLocal
例子:
public final class DataServerState {
*
* <b>public static final {@link ChannelLocal}<Boolean> loggedIn = new {@link ChannelLocal}<Boolean>() {
* protected Boolean initialValue(Channel channel) {
* return false;
* }
* }</b>
* ...
* }
*
* {@code @Sharable}
* public class DataServerHandler extends {@link SimpleChannelHandler} {
*
* {@code @Override}
* public void messageReceived({@link ChannelHandlerContext} ctx, {@link MessageEvent} e) {
* Channel ch = e.getChannel();
* Object o = e.getMessage();
* if (o instanceof LoginMessage) {
* authenticate((LoginMessage) o);
* <b>DataServerState.loggedIn.set(ch, true);</b>
* } else (o instanceof GetDataMessage) {
* if (<b>DataServerState.loggedIn.get(ch)</b>) {
* ctx.getChannel().write(fetchSecret((GetDataMessage) o));
* } else {
* fail();
* }
* }
* }
* ...
* }
*
* // Print the remote addresses of the authenticated clients:
* {@link ChannelGroup} allClientChannels = ...;
* for ({@link Channel} ch: allClientChannels) {
* if (<b>DataServerState.loggedIn.get(ch)</b>) {
* System.out.println(ch.getRemoteAddress());
* }
* }
* </pre>
5.ChannelPipeline
channelPipeline是一系列channelHandler的集合,他参照J2ee中的Intercepting Filter模式来实现的,
让用户完全掌握如果在一个handler中处理事件,同时让pipeline里面的多个handler可以相互交互。
Intercepting Filter:http://java.sun.com/blueprints/corej2eepatterns/Patterns/InterceptingFilter.html
对于每一个channel都需要有相应的channelPipeline,当为channel设置了channelPipeline后就不能再为channel重新设置
channelPipeline。此外建议的做法的通过Channels 这个帮助类来生成ChannelPipeline 而不是自己去构建ChannelPipeline
通常pipeLine 添加多个handler,是基于业务逻辑的
比如下面
{@link ChannelPipeline} p = {@link Channels}.pipeline();
* p.addLast("1", new UpstreamHandlerA());
* p.addLast("2", new UpstreamHandlerB());
* p.addLast("3", new DownstreamHandlerA());
* p.addLast("4", new DownstreamHandlerB());
* p.addLast("5", new SimpleChannelHandler());
upstream event 执行的handler按顺序应该是 125
downstream event 执行的handler按顺序应该是 543
SimpleChannelHandler 是同时实现了 ChannelUpstreamHandler和ChannelDownstreamHandler的类
上面只是具有逻辑,如果数据需要通过格式来进行编码的话,那需要这些写
* {@link ChannelPipeline} pipeline = {@link Channels#pipeline() Channels.pipeline()};
* pipeline.addLast("decoder", new MyProtocolDecoder());
* pipeline.addLast("encoder", new MyProtocolEncoder());
* pipeline.addLast("executor", new {@link ExecutionHandler}(new {@link OrderedMemoryAwareThreadPoolExecutor}(16, 1048576, 1048576)));
* pipeline.addLast("handler", new MyBusinessLogicHandler());
其中:
Protocol Decoder - 将binary转换为java对象
Protocol Encoder - 将java对象转换为binary
ExecutionHandler - applies a thread model.
Business Logic Handler - performs the actual business logic(e.g. database access)
虽然不能为channel重新设置channelPipeline,但是channelPipeline本身是thread-safe,因此你可以在任何时候为channelPipeline添加删除channelHandler
需要注意的是,下面的代码写法不能达到预期的效果
* public class FirstHandler extends {@link SimpleChannelUpstreamHandler} {
*
* {@code @Override}
* public void messageReceived({@link ChannelHandlerContext} ctx, {@link MessageEvent} e) {
* // Remove this handler from the pipeline,
* ctx.getPipeline().remove(this);
* // And let SecondHandler handle the current event.
* ctx.getPipeline().addLast("2nd", new SecondHandler());
* ctx.sendUpstream(e);
* }
* }
前提现在Pipeline只有最后一个FirstHandler,
上面明显是想把FirstHandler从Pipeline中移除,然后添加SecondHandler。而pipeline需要只要有一个Handler,因此如果想到到达这个效果,那么可以
先添加SecondHandler,然后在移除FirstHandler。
6.ChannelFactory
channel的工厂类,也就是用来生成channel的类,ChannelFactory根据指定的通信和网络来生成相应的channel,比如
NioServerSocketChannelFactory生成的channel是基于NIO server socket的。
当一个channel创建后,ChannelPipeline将作为参数附属给该channel。
对于channelFactory的关闭,需要做两步操作
第一,关闭所有该factory产生的channel包括子channel。通常调用ChannelGroup#close()。
第二,释放channelFactory的资源,调用releaseExternalResources()
7.ChannelGroup
channel的组集合,他包含一个或多个open的channel,closed channel会自动从group中移除,一个channel可以在一个或者多个channelGroup
如果想将一个消息广播给多个channel,可以利用group来实现
比如:
{@link ChannelGroup} recipients = new {@link DefaultChannelGroup}()
recipients.add(channelA);
recipients.add(channelB);
recipients.write(ChannelBuffers.copiedBuffer("Service will shut down for maintenance in 5 minutes.",CharsetUtil.UTF_8));
当ServerChannel和非ServerChannel同时都在channelGroup中的时候,任何io请求的操作都是先在ServerChannel中执行再在其他Channel中执行。
这个规则对关闭一个server非常适用。
8.ChannelFuture
在netty中,所有的io传输都是异步,所有那么在传送的时候需要数据+状态来确定是否全部传送成功,而这个载体就是ChannelFuture。
9.ChannelGroupFuture
针对一次ChannelGroup异步操作的结果,他和ChannelFuture一样,包括数据和状态。不同的是他由channelGroup里面channel的所有channelFuture
组成。
10.ChannelGroupFutureListener
针对ChannelGroupFuture的监听器,同样建议使用ChannelGroupFutureListener而不是await();
11.ChannelFutureListener
ChannelFuture监听器,监听channelFuture的结果。
12.ChannelFutureProgressListener
监听ChannelFuture处理过程,比如一个大文件的传送。而ChannelFutureListener只监听ChannelFuture完成未完成
13.ChannelHandlerContext
如何让handler和他的pipeLine以及pipeLine中的其他handler交换,那么就要用到ChannelHandlerContext,
ChannelHandler可以通过ChannelHandlerContext的sendXXXstream(ChannelEvent)将event传给最近的handler
可以通过ChannelHandlerContext的getPipeline来得到Pipeline,并修改他,ChannelHandlerContext还可以存放一下状态信息attments。
一个ChannelHandler实例可以有一个或者多个ChannelHandlerContext
14.ChannelPipelineFactory
产生ChannelPipe的工厂类
15.ChannelState
记载channel状态常量
Netty 是JBoss旗下的io传输的框架,他利用java里面的nio来实现高效,稳定的io传输。
作为io传输,就会有client和server,下面我们看看用netty怎样写client和server
Client:
需要做的事情:
1.配置client启动类
ClientBootstrap bootstrap = new ClientBootstrap(..)
2.根据不同的协议或者模式为client启动类设置pipelineFactory。
这里telnet pipline Factory 在netty中已经存在,所有直接用
bootstrap.setPipelineFactory(new TelnetClientPipelineFactory());
也可以自己定义
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(
new DiscardClientHandler(firstMessageSize));
}
});
这里DiscardClientHandler 就是自己定义的handler,他需要
public class DiscardServerHandler extends SimpleChannelUpstreamHandler
继承SimpleChannelUpstreamHandler 来实现自己的handler。这里DiscardClientHandler
是处理自己的client端的channel,他的
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
// Server is supposed to send nothing. Therefore, do nothing.
}
可以看到Discard client不需要接受任何信息
3.连接server
ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port));
这里解释一下channelFuture:
在Netty中所有的io操作都是异步的,这也就是意味任何io访问,那么就立即返回处理,并且不能确保
返回的数据全部完成。因此就出现了channelFuture,channelFuture在传输数据时候包括数据和状态两个
部分。他只有Uncompleted和Completed
既然netty io是异步的,那么如何知道channel传送完成有两种方式,一种添加监听器
addListener(ChannelFutureListener) 还有一种直接调用await()方法,这两种方式
有下面的区别
监听器:是以事件模式的,因此代码就需要用事件模式的样式去写,相当复杂,但他是non-blocking模式的
性能方面要比await方法好,而且不会产生死锁情况
await(): 直接方法调用,使用简单,但是他是blocking模式,性能方面要弱而且会产生死锁情况
不要在ChannelHandler 里面调用await(),这是因为通常在channelHandler里的event method是被i/o线程调用的
(除非ChannelPipeline里面有个ExecutionHandler),那么如果这个时候用await就容易产生死锁。
错误样例:
// BAD - NEVER DO THIS
* {@code @Override}
* public void messageReceived({@link ChannelHandlerContext} ctx, {@link MessageEvent} e) {
* if (e.getMessage() instanceof GoodByeMessage) {
* {@link ChannelFuture} future = e.getChannel().close();
* future.awaitUninterruptibly();
* // Perform post-closure operation
* // ...
* }
* }
*
正确样例:
* // GOOD
* {@code @Override}
* public void messageReceived({@link ChannelHandlerContext} ctx, {@link MessageEvent} e) {
* if (e.getMessage() instanceof GoodByeMessage) {
* {@link ChannelFuture} future = e.getChannel().close();
* future.addListener(new {@link ChannelFutureListener}() {
* public void operationComplete({@link ChannelFuture} future) {
* // Perform post-closure operation
* // ...
* }
* });
* }
* }
虽然await调用比较危险,但是你确保不是在一个i/o 线程中调用该方法,毕竟await方法还是很简洁方便的,如果
调用该方法是在一个i/o 线程,那么就会抛出 IllegalStateException
await的timeout和i/o timeout区别
需要注意的是这两个timeout是不一样的, #await(long),#await(long, TimeUnit), #awaitUninterruptibly(long),
#awaitUninterruptibly(long, TimeUnit) 这里面的timeout也i/o timeout 没有任何关系,如果io timeout,那么
channelFuture 将被标记为completed with failure,而await的timeout 与future完全没有关系,只是await动作的
timeout。
错误代码
* // BAD - NEVER DO THIS
* {@link ClientBootstrap} b = ...;
* {@link ChannelFuture} f = b.connect(...);
* f.awaitUninterruptibly(10, TimeUnit.SECONDS);
* if (f.isCancelled()) {
* // Connection attempt cancelled by user
* } else if (!f.isSuccess()) {
* // You might get a NullPointerException here because the future
* // might not be completed yet.
* f.getCause().printStackTrace();
* } else {
* // Connection established successfully
* }
*
正确代码
* // GOOD
* {@link ClientBootstrap} b = ...;
* // Configure the connect timeout option.
* <b>b.setOption("connectTimeoutMillis", 10000);</b>
* {@link ChannelFuture} f = b.connect(...);
* f.awaitUninterruptibly();
*
* // Now we are sure the future is completed.
* assert f.isDone();
*
* if (f.isCancelled()) {
* // Connection attempt cancelled by user
* } else if (!f.isSuccess()) {
* f.getCause().printStackTrace();
* } else {
* // Connection established successfully
* }
4.等待或监听数据全部完成
如: future.getChannel().getCloseFuture().awaitUninterruptibly();
5.释放连接等资源
bootstrap.releaseExternalResources();
Server:
1.配置server
ServerBootstrap bootstrap = new ServerBootstrap(
new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool()));
2.设置pipeFactory
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(new EchoServerHandler());
}
});
或者
bootstrap.setPipelineFactory(new HttpServerPipelineFactory());
3.绑定sever端端口
bootstrap.bind(new InetSocketAddress(8080));
org.jboss.netty.bootstrap
本身 Netty 可以作为一个server存在的,因此他存在启动入口,他具有client启动,server启动以及connectionless 启动(比如UDP)
1.基类bootstrap:他包含ChannelFactory,ChannelPipeline,ChannelPipelineFactory。
ClientBootstrap: 有connect()方法
ConnectionlessBootstrap:有connect(),bind()方法
ServerBootstrap:有bind()方法
2.org.jboss.netty.buffer
netty自己提供了buffer 来取代nio 中的java.nio.ByteBuffer,他与nio中的byteBuffer相比,有下面的几个特点
1>可以根据自己需要自己定义buffer type
2>只是buffer type改变而不拷贝buffer的内容,这样可以减少开销
3>动态大小的buffer type 类似于stringbuffer
4>不需要调用flip()方法
5>更快的性能
3.org.jboss.netty.channel
channel的核心api,包括异步和事件驱动等各种传送接口
org.jboss.netty.channel.group
channel group,里面包含一系列open的channel
org.jboss.netty.channel.local
一种虚拟的运输方式,能允许同一个虚拟机上的两个部分可以互相通信
org.jboss.netty.channel.socket
TCP,UDP端口接口,主要继承channel
org.jboss.netty.channel.socket.nio
基于nio端口channel的具体实现
org.jboss.netty.channel.socket.oio
基于老的io端口的channel的具体实现
org.jboss.netty.channel.socket.http
基于http的客户端和相应的server端的实现,可以在有防火墙的情况下进行工作
需要做的事情
a. 将http tunnel 作为servlet进行配置
web.xml
<servlet>
* <servlet-name>NettyTunnelingServlet</servlet-name>
* <servlet-class>org.jboss.netty.channel.socket.http.HttpTunnelingServlet</servlet-class>
* <!--
* The name of the channel, this should be a registered local channel.
* See LocalTransportRegister.
* -->
* <init-param>
* <param-name>endpoint</param-name>
* <param-value>local:myLocalServer</param-value>
* </init-param>
* <load-on-startup>1</load-on-startup>
* </servlet>
*
* <servlet-mapping>
* <servlet-name>NettyTunnelingServlet</servlet-name>
* <url-pattern>/netty-tunnel</url-pattern>
* </servlet-mapping>
接下来需要将你的基于netty的server app绑定到上面的http servlet
你可以这样写
*
* public class LocalEchoServerRegistration {
*
* private final ChannelFactory factory = new DefaultLocalServerChannelFactory();
* private volatile Channel serverChannel;
*
* public void start() {
* ServerBootstrap serverBootstrap = new ServerBootstrap(factory);
* EchoHandler handler = new EchoHandler();
* serverBootstrap.getPipeline().addLast("handler", handler);
*
* // Note that "myLocalServer" is the endpoint which was specified in web.xml.
* serverChannel = serverBootstrap.bind(new LocalAddress("myLocalServer"));
* }
*
* public void stop() {
* serverChannel.close();
* }
* }
然后在Ioc framework(JBoss Microcontainer,Guice,Spring)中定义bean
<bean name="my-local-echo-server"
class="org.jboss.netty.example.http.tunnel.LocalEchoServerRegistration" />
这样http servlet 就可以了
b. 连接http tunnel
构造client
* ClientBootstrap b = new ClientBootstrap(
* new HttpTunnelingClientSocketChannelFactory(
* new NioClientSocketChannelFactory(...)));
*
* // Configure the pipeline (or pipeline factory) here.
* ...
*
* // The host name of the HTTP server
* b.setOption("serverName", "example.com");
* // The path to the HTTP tunneling Servlet, which was specified in in web.xml
* b.setOption("serverPath", "contextPath/netty-tunnel");
* b.connect(new InetSocketAddress("example.com", 80);
4.org.jboss.netty.container
各种容器的兼容
org.jboss.netty.container.microcontainer
JBoss Microcontainer 整合接口
org.jboss.netty.container.osgi
OSGi framework 整合接口
org.jboss.netty.container.spring
Spring framework 整合接口
5.org.jboss.netty.handler
处理器
org.jboss.netty.handler.codec.base64
Base64 编码
org.jboss.netty.handler.codec.compression
压缩格式
org.jboss.netty.handler.codec.embedder
嵌入模式下编码和解码,即使没有真正的io环境也能使用
org.jboss.netty.handler.codec.frame
可扩展的接口,重新评估基于流的数据的排列和内容
org.jboss.netty.handler.codec.http.websocket
websocket相关的编码和解码,
参考
http://en.wikipedia.org/wiki/Web_Sockets
org.jboss.netty.handler.codec.http
http的编码解码以及类型信息
org.jboss.netty.handler.codec.oneone
一个对象到另一对象的自定义抽象接口,如果有自己编码需要继承该抽象类
org.jboss.netty.handler.codec.protobuf
Google Protocol Buffers的编码解码
Google Protocol Buffers参考下面
http://code.google.com/p/protobuf/
org.jboss.netty.handler.codec.replay
org.jboss.netty.handler.codec.rtsp
Real_Time_Streaming_Protocol的编码解码
Real_Time_Streaming_Protocol 参考下面wiki
http://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol
org.jboss.netty.handler.codec.serialization
将序列化的对象转换到byte buffer的相关实现
org.jboss.netty.handler.codec.string
字符串编码解码,比如utf8编码等,继承oneone包的接口
org.jboss.netty.handler.execution
基于java.util.concurrent.Executor的实现
org.jboss.netty.handler.queue
将event存入内部队列的处理
org.jboss.netty.handler.ssl
基于javax.net.ssl.SSLEngine的SSL以及TLS实现
参考
http://en.wikipedia.org/wiki/Transport_Layer_Security
org.jboss.netty.handler.stream
异步写入大数据,不会产生outOfMemory 也不会花费很多内存
org.jboss.netty.handler.timeout
通过jboss.netty.util.Timer来对读写超时或者闲置链接的通知
6.org.jboss.netty.logging
根据不同的log framework 实现的类
7.org.jboss.netty.util
nettyutil类
org.jboss.netty.util.internal
netty内部util类,不被外部使用
JSF 探索 (1)
虽然JSF已经出来好久了,而且好像感觉不是很潮流的框架了,然后当我看见他的时候,我觉得他的思想很好,而且随着手机等client多样化的出现,我相信人们
必将重新拾起JSF的思想。
JSF说白了是事件驱动,web以前的时代我估计大部分都是事件驱动的开发模式,而web的出现,使人们更多关注浏览器上面的渲染,以及servlet和http的操作上。
web2.0的出现,使人们感觉在浏览器上的操作更像以前c/s结构的操作,而手机client的热捧更能体现未来多种客户端访问server的模式即将到来,也许大家开始
在想如何做新的框架,其实这种想法已经out了,JSF里面早就有这个思想了,你只需要去关注JSF不就ok了?
任何《* in action》的书总是让programmer 兴奋,同样JSF in Action也是一本不错的书,让我们一起去看看它把。
1.JSF 关键词
UI component :静态对象,它保存在server端,其实本身UI Component是javabean对象,他具有javabean具备的属性,methods和events。通常多个component被组织为一个页面的视图
Renderer:渲染器,renderer被用作渲染component,以及将用户的输入转换为component的值。特别当不同类型的client端,比如手机,那么对应的就有手机相应格式的renderer。
Validator:负责验证用户输入值的可靠性,单个UI component可以对应一个或多个Validator.
Backing beans:后台的javabean,做一些逻辑处理什么的,存储component的值,实现事件监听器,他可以引用component。
Converter:显示转换器,比如时间格式的显示,一个component可以对应一个converter
Events and listeners:事件和监听器
Messages:显示给用户的信息
Navigation:一个页面到另外一个页面的向导。
2.JSF请求的六个阶段
Phase 1: Restore View
构建视图页面供用户输入或查看
Phase 2: Apply Request Values
获取请求值
Phase 3: Process Validations
可以对请求值进行验证
Phase 4: Update Model Values
更新back bean 的值
Phase 5: Invoke Application
调用应用,执行相应的监听器
Phase 6: Render Response
将response根据要求进行包装后返回给用户,这里的包装是指知道的页面技术,比如html,比如手机展现技术
3.client and server component id
server component id可以在component 的id属性中显性填写id,以便在服务端能唯一标识该组件
client id 是继承server component id,他最后体现的也是html 中的标签,而id也为对应标签的id,与server component id不同的是,他有继承关系,比如
<form id="myForm" method="post"
action="/jia-standard-components/client_ids.jsf"
enctype="application/x-www-form-urlencoded">
<p>
<input type="text" name="myForm:_id1" />
</p>
<p>
<input id="myForm:inputText" type="text" name="myForm:inputText" />
</p>
...
</form>
而对应的服务端
<p>
<h:outputText id="outputText" value="What are you looking at?"/>
</p>
<h:form id="myForm">
<p>
<h:inputText/>
</p>
<p>
<h:inputText id="inputText"/>
</p>
...
</h:form>
4.JSF EL 表达语言: 他是基于JSTL的描述
此外Jsp中application,session,page 四个scoped变量,只有page jsf中不支持
除了上面三个scoped变量,jsf还有下面一些隐性的变量
applicationScope:从application中获取对象,比如#{applicationScope.myVariable} myVariable对象存放在application中
cookie:从cookie中获取数据
facesContext:这个jsp2.0中没有,当前请求的faceContext实例
header:http head头信息,比如#{header['User-Agent']}
headerValues:多个head头信息的载体 比如#{headerValues['Accept-Encoding'][3]}
initParam:初始化的一些参数比如servlet context initialization parameters 可以这样使用#{initParam.adminEmail}
param:等同jsp中的request.getParameter() 用法#{param.address}
paramValues:等同jsp中的request.getParameterValues 用法#{param.address[2]}
requestScope等同于jsp中reqeust 用法#{requestScope.user-Preferences}
sessionScope等同于jsp中session 用法#{sessionScope['user']}
view 这个变量jsp2.0中没有,它表示当前视图,它有三个属性viewId,renderKitId, and locale 用法#{view.locale}
5.需要的jar包和文件
构建jsf环境需要jsf-api.jar,jsf-impl.jar,jstl.jar,standard.jar 以及配置文件faces-config.xml
需要在web.xml 中配置faceservlet
<web-app>
...
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
...
</web-app>
web.xml 中jsf定义的上下文的参数有
javax.faces.CONFIG_FILES
javax.faces.DEFAULT_SUFFIX
javax.faces.LIFECYCLE_ID
javax.faces.STATE_SAVING_METHOD
RI-specific configuration parameters:
com.sun.faces.NUMBER_OF_VIEWS_IN_SESSION
com.sun.faces.validateXml
com.sun.faces.verifyObjects
比如
<web-app>
...
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value>
</context-param>
<context-param>
<param-name>javax.faces.CONFIG_FILES</param-name>
<param-value>/WEB-INF/navigation.xml,/WEB-INF/RegistrationWizard.xml</
param-value>
</context-param>
...
<web-app>
除了web.xml jsf相关的参数,本身jsf具有faces-config.xml 具体里面的配置属性可以查看配置文件,可以找可视化工具进行配置。
Java 理论与实践: 正确使用 Volatile 变量
volatile 变量使用指南
简介: Java™ 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。在这期的 Java 理论与实践 中,Brian Goetz 将介绍几种正确使用 volatile 变量的模式,并针对其适用性限制提出一些建议。
查看本系列更多内容
发布日期: 2007 年 7 月 05 日
级别: 中级
访问情况 237 次浏览
建议: 0 (添加评论)
Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized
”;与 synchronized
块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized
的一部分。本文介绍了几种有效使用 volatile 变量的模式,并强调了几种不适合使用 volatile 变量的情形。
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
Volatile 变量
Volatile 变量具有 synchronized
的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。
出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。
正确使用 volatile 变量的条件
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++
)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x
的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized
那样普遍适用于实现线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。
清单 1. 非线程安全的数值范围类
@NotThreadSafe
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
|
这种方式限制了范围的状态变量,因此将 lower
和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全;从而仍然需要使用同步。否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLower
和 setUpper
的话,则会使范围处于不一致的状态。例如,如果初始状态是 (0, 5)
,同一时间内,线程 A 调用 setLower(4)
并且线程 B 调用 setUpper(3)
,显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3)
—— 一个无效值。至于针对范围的其他操作,我们需要使 setLower()
和 setUpper()
操作原子化 —— 而将字段定义为 volatile 类型是无法实现这一目的的。
性能考虑
使用 volatile 变量的主要原因是其简易性:在某些情形下,使用 volatile 变量要比使用相应的锁简单得多。使用 volatile 变量次要原因是其性能:某些情况下,volatile 变量同步机制的性能要优于锁。
很难做出准确、全面的评价,例如 “X 总是比 Y 快”,尤其是对 JVM 内在的操作而言。(例如,某些情况下 VM 也许能够完全删除锁机制,这使得我们难以抽象地比较 volatile
和 synchronized
的开销。)就是说,在目前大多数的处理器架构上,volatile 读操作开销非常低 —— 几乎和非 volatile 读操作一样。而 volatile 写操作的开销要比非 volatile 写操作多很多,因为要保证可见性需要实现内存界定(Memory Fence),即便如此,volatile 的总开销仍然要比锁获取低。
volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。
正确使用 volatile 的模式
很多并发性专家事实上往往引导用户远离 volatile 变量,因为使用它们要比使用锁更加容易出错。然而,如果谨慎地遵循一些良好定义的模式,就能够在很多场合内安全地使用 volatile 变量。要始终牢记使用 volatile 的限制 —— 只有在状态真正独立于程序内其他内容时才能使用 volatile —— 这条规则能够避免将这些模式扩展到不安全的用例。
模式 #1:状态标志
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
很多应用程序包含了一种控制结构,形式为 “在还没有准备好停止程序时再执行一些工作”,如清单 2 所示:
清单 2. 将 volatile 变量作为状态标志使用
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
|
很可能会从循环外部调用 shutdown()
方法 —— 即在另一个线程中 —— 因此,需要执行某种同步来确保正确实现 shutdownRequested
变量的可见性。(可能会从 JMX 侦听程序、GUI 事件线程中的操作侦听程序、通过 RMI 、通过一个 Web 服务等调用)。然而,使用synchronized
块编写循环要比使用清单 2 所示的 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。
这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested
标志从 false
转换为 true
,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能扩展(从 false
到 true
,再转换到 false
)。此外,还需要某些原子状态转换机制,例如原子变量。
模式 #2:一次性安全发布(one-time safe publication)
缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原语值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。
实现安全发布对象的一种技术就是将对象引用定义为 volatile 类型。清单 3 展示了一个示例,其中后台线程在启动阶段从数据库加载一些数据。其他代码在能够利用这些数据时,在使用之前将检查这些数据是否曾经发布过。
清单 3. 将 volatile 变量用于一次性安全发布
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;
public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}
public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}
|
如果 theFlooble
引用不是 volatile 类型,doWork()
中的代码在解除对 theFlooble
的引用时,将会得到一个不完全构造的 Flooble
。
该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布之后永远不会被修改)。volatile 类型的引用可以确保对象的发布形式的可见性,但是如果对象的状态在发布后将发生更改,那么就需要额外的同步。
模式 #3:独立观察(independent observation)
安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
使用该模式的另一种应用程序就是收集程序的统计信息。清单 4 展示了身份验证机制如何记忆最近一次登录的用户的名字。将反复使用 lastUser
引用来发布值,以供程序的其他部分使用。
清单 4. 将 volatile 变量用于多个独立观察结果的发布
public class UserManager {
public volatile String lastUser;
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}
|
该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布后不会更改。使用该值的代码需要清楚该值可能随时发生变化。
模式 #4:“volatile bean” 模式
volatile bean 模式适用于将 JavaBeans 作为“荣誉结构”使用的框架。在 volatile bean 模式中,JavaBean 被用作一组具有 getter 和/或 setter 方法 的独立属性的容器。volatile bean 模式的基本原理是:很多框架为易变数据的持有者(例如 HttpSession
)提供了容器,但是放入这些容器中的对象必须是线程安全的。
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile
时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。清单 5 中的示例展示了遵守 volatile bean 模式的 JavaBean:
清单 5. 遵守 volatile bean 模式的 Person 对象
@ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
|
volatile 的高级模式
前面几节介绍的模式涵盖了大部分的基本用例,在这些模式中使用 volatile 非常有用并且简单。这一节将介绍一种更加高级的模式,在该模式中,volatile 将提供性能或可伸缩性优势。
volatile 应用的的高级模式非常脆弱。因此,必须对假设的条件仔细证明,并且这些模式被严格地封装了起来,因为即使非常小的更改也会损坏您的代码!同样,使用更高级的 volatile 用例的原因是它能够提升性能,确保在开始应用高级模式之前,真正确定需要实现这种性能获益。需要对这些模式进行权衡,放弃可读性或可维护性来换取可能的性能收益 —— 如果您不需要提升性能(或者不能够通过一个严格的测试程序证明您需要它),那么这很可能是一次糟糕的交易,因为您很可能会得不偿失,换来的东西要比放弃的东西价值更低。
模式 #5:开销较低的读-写锁策略
目前为止,您应该了解了 volatile 的功能还不足以实现计数器。因为 ++x
实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。
然而,如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。清单 6 中显示的线程安全的计数器使用 synchronized
确保增量操作是原子的,并使用 volatile
保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
清单 6. 结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”
@ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}
|
之所以将这种技术称之为 “开销较低的读-写锁” 是因为您使用了不同的同步机制进行读写操作。因为本例中的写操作违反了使用 volatile 的第一个条件,因此不能使用 volatile 安全地实现计数器 —— 您必须使用锁。然而,您可以在读操作中使用 volatile 确保当前值的可见性,因此可以使用锁进行所有变化的操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。然而,要随时牢记这种模式的弱点:如果超越了该模式的最基本应用,结合这两个竞争的同步机制将变得非常困难。
结束语
与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile
代替 synchronized
来简化代码。然而,使用 volatile
的代码往往比使用锁的代码更加容易出错。本文介绍的模式涵盖了可以使用 volatile
代替synchronized
的最常见的一些用例。遵循这些模式(注意使用时不要超过各自的限制)可以帮助您安全地实现大多数用例,使用 volatile 变量获得更佳性能。
参考资料
学习
讨论
关于作者
Java NIO---Channel部分
Channel 是连接buffer和Io设备之间的管道,当然channel也可以一头连接channel
Basic Channel Interface:
public interface Channel
{
public boolean isOpen( );
public void close( ) throws IOException;
}
1.open channel
channel作为i/o 设备的管道,分为两种,一种文件(file),另一种是端口(Socket)
file相关的只有fileChannel
socket相关的有SocketChannel, ServerSocketChannel和DatagramChannel.
socket channel可以通过factory创建新的socket channel
比如
SocketChannel sc = SocketChannel.open( );
sc.connect (new InetSocketAddress ("somehost", someport));
ServerSocketChannel ssc = ServerSocketChannel.open( );
ssc.socket( ).bind (new InetSocketAddress (somelocalport));
而fileChannel 则不能这样,他是在一个打开的andomAccessFile, FileInputStream或者FileOutputStream
对象上getChannel()
比如
RandomAccessFile raf = new RandomAccessFile ("somefile", "r");
FileChannel fc = raf.getChannel( );
2.Using Channels
三个接口:
public interface ReadableByteChannel
extends Channel
{
public int read (ByteBuffer dst) throws IOException;
}
public interface WritableByteChannel
extends Channel
{
public int write (ByteBuffer src) throws IOException;
}
public interface ByteChannel
extends ReadableByteChannel, WritableByteChannel
{
}
需要注意的是 fileChannel 虽然也有write,read方法,但是他们能否调用write read方法是由file相关的访问权限决定的。
channel 有blocking和nonblocking两种模式,nonblocking是指永远不会将一个正在调用的线程sleep,他们被请求的结果要么
立刻完成,要么返回一个nothing的结果。
只有stream-oriented的channels(比如sockets and pipes)可以用nonblocking模式
3.Closing Channels
和buffer不一样,channel不能被重用,用完之后一定要close,此外当一个线程被设置为interrupt状态,当该线程试图访问某个channel
的话,该channel将立刻关闭。
4.Scatter/Gather
当我们在多个buffer上执行一个i/O操作的时候,我们需要将多个buffer放在一个buffer数组一并让channel来处理
Scatter/Gather interface:
public interface ScatteringByteChannel
extends ReadableByteChannel
{
public long read (ByteBuffer [] dsts)
throws IOException;
public long read (ByteBuffer [] dsts, int offset, int length)
throws IOException;
}
public interface GatheringByteChannel
extends WritableByteChannel
{
public long write(ByteBuffer[] srcs)
throws IOException;
public long write(ByteBuffer[] srcs, int offset, int length)
throws IOException;
}
例子:
ByteBuffer header = ByteBuffer.allocateDirect (10);
ByteBuffer body = ByteBuffer.allocateDirect (80);
ByteBuffer [] buffers = { header, body };
int bytesRead = channel.read (buffers);
5.File Channels
filechannel 只能是blocking模式也就是被锁住模式,不能是nonblocking模式
public abstract class FileChannel
extends AbstractChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
// This is a partial API listing
public abstract void truncate (long size)
public abstract void force (boolean metaData)
}
fileChannel 中有truncate和force 两个方法
truncate (long size)是以给定的size来切断file
force (boolean metaData) file属性的修改立刻影响到disk上
6.File Locking
file 锁虽然在fileChannel中提出,但是需要注意本身lock是和file相关,而且不同的操作系统提供的file也有不同,和channel无关的
public abstract class FileChannel
extends AbstractChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
// This is a partial API listing
public final FileLock lock( )
public abstract FileLock lock (long position, long size,
boolean shared)
public final FileLock tryLock( )
public abstract FileLock tryLock (long position, long size,
boolean shared)
}
lock 有三个参数,第一个是开始锁住的file的postion, 第二个是锁住的file大小,前面这两个参数就能定义了一块lock范围,第三个是
显示lock是shared(true)类型还是exclusive(false)类型,
没有参数的lock()其实等同于fileChannel.lock (0L, Long.MAX_VALUE, false);
FileLock 本身是线程安全的,可以多个线程同时访问
tryLock() 和Lock()相似,当某个lock因为其他原因不能被获取,那么就要用tryLock() 这个时候就返回null
FileLock总关联一个特定的channel,可以通过channel()获取相关联的channel,当FileLock release(),那么对于的channel也就相应的close();
7.Memory-Mapped Files
Memory-mapped files 一般效率更高,而且因为他是和操作系统相关的,所有他不会消费jvm分配的内存
通过map()来建立MappedByteBuffer,
mapped buffers和lock有点不同,lock和产生它的channel绑定,如果channel closed那么lock也就消失
而mapped buffers本身也没有ummap方法,他是通过自身不再被引用然后被系统垃圾回收的。
8.Channel-to-Channel Transfers
channel直接的传输可以提供更好的效率
public abstract class FileChannel
extends AbstractChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
// This is a partial API listing
public abstract long transferTo (long position, long count,
WritableByteChannel target)
Java NIO
90
public abstract long transferFrom (ReadableByteChannel src,
long position, long count)
}
这个是能在filechannel之间使用,两个socketchannel之间不能直接使用transferTo和transferFrom,但是因为socketchannel继承WritableByteChannel and ReadableByteChannel,所有可以将一个文件的内容transferTo socketChannel或直接通过transferFrom从socketChannel中读取数据
9.Socket Channels
对应以前一般一个线程对应一个socket,而在NIO中可以一个线程对应成百上千的socket,同时没有性能降低的问题
三种socket channel(DatagramChannel,SocketChannel, and ServerSocketChannel)
DatagramChannel和SocketChannel 有read和write的方法
而ServerSocketChannel 则是监听connection和创建SocketChannel,他本身不参与数据的传输
这三个channel都可以通过socket()方法获取到对应的Socket,ServerSocket,DatagramSocket
需要说明的是,socket不一样要绑定channel,通过传统的new 创建socket,那么getChannel()就会返回null
SocketChannel 有Nonblocking和blocking 两种模式
SocketChannel sc = SocketChannel.open( );
sc.configureBlocking (false); // nonblocking
...
if ( ! sc.isBlocking( )) {
doSomething (cs);
}
可以通过configureBlocking 来设置,true表示blocking mode 而false 表示nonblocking mode
对应大型应用并发处理的 建议使用nonblocking mode
看看下面的例子:
Socket socket = null;
Object lockObj = serverChannel.blockingLock( );
// have a handle to the lock object, but haven't locked it yet
// may block here until lock is acquired
synchronize (lockObj)
{
// This thread now owns the lock; mode can't be changed
boolean prevState = serverChannel.isBlocking( );
serverChannel.configureBlocking (false);
socket = serverChannel.accept( );
serverChannel.configureBlocking (prevState);
}
// lock is now released, mode is allowed to change
if (socket != null) {
doSomethingWithTheSocket (socket);
}
体验一下 blockingLock()和lock()的区别
SocketChannel 可以通过finishConnect( ),isConnectPending( ), or isConnected( )获取当前connection的状态
socket是面向流的,datagram是面向packet的
因此DatagramChannel 就是面向packet协议的channel,比如UDP/IP
10.pipe 方式 可以研究一下
Java NIO---buffer部分
最近想建一个网络传输的平台,查看到了Jboss 的Netty,而他们核心的传输是用了JDK 1.4以后的
NIO特性,因此借机会学习一下NIO
NIO主要有下面几大部分
Buffer:Io的操作少不了缓存,通过Buffer能大大提高传输的效率,同样NIO中也有Buffer的这部分
Buffer针对数据类型有相应的子类,他们都是继承Buffer class
比如CharBuffer,IntBuffer等等
需要说明的子类MappedByteBuffer 通过命名可以看出这个MappedByteBuffer有mapped+byte+Buffer组成
据我理解 这么mapped 是指memory-mapped
这里解释一下memory-mapped, 以为我们说的buffer 可能就是指物理上的内存,包括jdk 以前的io
一般情况下,buffer指物理上的内存没什么问题,可以物理内存毕竟比较有限,当需要很大buffer存放数据的时候
物理内存就不够,那么就引出一个虚拟内存的概念,就像现在的os 一样,也有虚拟内存的概念,虚拟内存本质上
是存在硬盘上的,只是物理内存存放的不是具体的数据而是虚拟内存上的地址,而具体的数据则是在虚拟内存上,这样
物理内存只是存放很多地址,这样就大大增加了buffer的size。当然如果你buffer的数据很小,也可以知道放入物理内存
因此我认为 这个memory-mapped 是指的用虚拟内存的哪种缓存模式
A.Buffer的 attribute
1.Capacity:buffer的大小,一个buffer按照某个Capacity创建后就不能修改
2.Limit:用了多少buffer
3.Position:指针,获取下一个元素的位置,操作get(),put(),这个position会自动更新(和iterator相似)
4.Mark:被标记的指针位置 通过mark()操作可以使mark = position 通过reset() 可以使position = mark
上面这几个属性应该符合下面条件
0 <= mark <= position <= limit <= capacity
B. fill:插入数据
用put() 方法可以插入一段数据 但是这个时候postion 在插入这段数据的尾部
C. flip:弹出数据
因为上面写入数据后,指针在尾部不是从0开始,那么我们需要
buffer.limit(buffer.position( )).position(0);
将limit 设置为当前的postion,limit 可以告诉方法这段数据什么时候结束,而具体可以调用方法
hasRemaining( ) 来判断是否数据结束。
同时将当前指针设置为0
NIO 提供了方法buffer.flip()完成上面的动作
另外方法rewind( )和flip相似 只是区别在于flip要修改limit,而rewind不修改limit
rewind 用处可以在已经flip后,还能重读这段数据
Buffer不是线程安全的,如果你想要多线程同时访问,那么需要用synchronization
D. Mark:让buffer记住某个指针,已备后面之用。
调用mark()就能将当前指针mark住
而调用reset()就能将当前指针回到mark了的指针位置,如果mark未定义,那么调用reset就会报InvalidMarkException
此外rewind( ), clear( ), and flip( )这些操作将会抛弃mark,这样clear()和reset()区别
clear表示清空buffer 而reset表示能将当前指针回到mark了的指针位置
E:Comparing:buffer 也是java的object sub class,所有buffer也能对象进行比较
public abstract class ByteBuffer
extends Buffer implements Comparable
{
// This is a partial API listing
public boolean equals (Object ob)
public int compareTo (Object ob)
}
两个buffer 认为相等,需要具备下面条件
1.buffer 类型相同
2.buffer里面保留的元素数量相同,buffer capacities不需要相同,这里需要注意保留的元素是指有效的元素,不是指buffer里面有的元素。
其实就从position到limit这段的元素,当然当前的postion和limit值都可以不相同,但这段数据要相同
3.remaining data 的数据顺序要相同,可以通过get()来循环获取出来后判断每个element都相同
如果同时符合上面三个条件,那么就表示buffer object 相等
F:Bulk Moves
如何在buffer中大块的数据移动,对性能起到关键作用
他的做法是get的时候,将一块数据取出放入数组里面,这样比起循环get()一个byte要效率高多了,那么对于块状数据总是有个指定的长度
这个长度就是指定数组的长度
public CharBuffer get (char [] dst, int offset, int length)
这里就是length。
如果只有参数为数组buffer.get (myArray);
那么其实也是指定了长度,只是长度为数组的长度,上面的语句等同于
buffer.get (myArray, 0, myArray.length);
针对上面,有可能buffer的data 小于myArray.length,这个时候如果取buffer 那么会报错
所以需要下面的写法
char [] bigArray = new char [1000];
// Get count of chars remaining in the buffer
int length = buffer.remaining( );
// Buffer is known to contain < 1,000 chars
buffer.get (bigArrray, 0, length);
// Do something useful with the data
processData (bigArray, length);
这是buffer<length
如果buffer>length 那需要loop:
char [] smallArray = new char [10];
while (buffer.hasRemaining( )) {
int length = Math.min (buffer.remaining( ), smallArray.length);
buffer.get (smallArray, 0, length);
processData (smallArray, length);
}
同样操作put是将array中的数据放入buffer,他同样有上面length的问题,需要用remaining来判断
G:创建Buffer: buffer class 不能直接new创建,他们都是abstract class,但是他们都有static factory,通过factory可以创建instances
比如
public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
// This is a partial API listing
public static CharBuffer allocate (int capacity)
public static CharBuffer wrap (char [] array)
public static CharBuffer wrap (char [] array, int offset,
int length)
public final boolean hasArray( )
public final char [] array( )
public final int arrayOffset( )
}
可以通过allocation或者wrapping来创建buffer
Alloction:允许分配私有buffer
CharBuffer charBuffer = CharBuffer.allocate (100);
wrapping:不允许分配私有buffer,显性提供存储
char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray);
而myArray 叫做buffer的backing array
通过hasArray( ) 判断buffer是否存在backing array
如果为true,可通过array()得到array的引用
H:Duplicating Buffers 复制buffer
Duplicat() 创建一个和原来一样的新的buffer,他们都有各自的postion,limit,mark, 但是共享同样的数据,也就是说任何一个buffer被修改了
他们看到的数据也就修改了。创建的新buffer继承了老buffer的属性,比如如果老buffer为readonly,那么新的也是readonly
asReadOnlyBuffer( ) 和Duplicat()相似,只是区别asReadOnlyBuffer( ) 创建的buffer总是readonly
slice() 和Duplicat()也相似,不同之处在于slice() 将修改buffer的capacity=(limit - position),同时新buffer以老buffer的当前postion作为开始
指针。
I:Byte Buffers 可以获取不同类型的buffer,另外byte本身也能排序
J:Direct Buffers: 对于操作系统来说,内存存储的数据不一定是连续的。而Direct buffer直接用于和io设备进行交互,对于io设备操作的buffer只能是
direct buffer,如果是nondirect ByteBuffer需要写入设备,那么他首先是创建一个临时的direct byteBuffer,然后将内容考入这个临时direct buffer,
接着进行底层的io操作,完成io操作后,临时buffer将被垃圾回收。
ByteBuffer.allocateDirect()创建direct buffer,isDirect( )则判断buffer是否是direct buffer
K:View Buffers 当一堆数据被收到后需要先查看他,然后才能确定是send还是怎么处理,这个时候就要引入View buffer
View Buffer拥有自己的属性,比如postion,limit,mark,但是他是和初始buffer共享数据的,这个和duplicated,sliced相似,但是view Buffer
能将raw bytes 映射为指定的基础类型buffer,这个也是查看的具体内容了,我们也可以认为是byte buffer向其他基础类型buffer的转换
public abstract class ByteBuffer
extends Buffer implements Comparable
{
// This is a partial API listing
public abstract CharBuffer asCharBuffer( );
public abstract ShortBuffer asShortBuffer( );
public abstract IntBuffer asIntBuffer( );
public abstract LongBuffer asLongBuffer( );
public abstract FloatBuffer asFloatBuffer( );
public abstract DoubleBuffer asDoubleBuffer( );
}