-
JNI即是Java Native Interface,主要用来Java和其他语言之间的交互,多半是和平台依赖的调用、其他高级语言的库甚至是低级语言交互。相信大家平时在JDK中看到很多native的方法,但是自己写JNI应用的会确很少。这次因为在进行开发中,遇到需要Java调用一个现成的成熟的功能完善的C语言应用库,因此 JNI便成了Java和C库之间的桥的功能。既然是初探,那么下面我们就从HelloWorld开始吧:虽然Java是一个跨平台的语言,但是C语言是一个和平台密切相关的语言;所以针对Unix、Linux、Windows等不同的操作系统,C语言的预编译,动态库的创建等都有一些差别,也需要我们分别进行处理之。下面仅基于Sun的Unix平台Sun Solaris开发为例。
1.首先声明第一个Java native程序:HelloWorld.java。
public class HelloWorld {
static {
init();
}
private native int hello();
public int sayHello() {
return hello();
}
/**
* @param args
*/
public static void main(String[] args) {
HelloWorld hw = new HelloWorld();
System.out.println("Say in Java:" + hw.sayHello());
}
private static void init() {
System.out.println("Starting to load HelloWorld lib.");
try {
System.loadLibrary("HelloWorld");
} catch (Throwable t) {
System.out.println("Load unsuccessfully.");
t.printStackTrace();
System.exit(-1);
}
System.out.println("Load successfully.");
}
}
2.编译该Java文件。
javac HelloWorld.java
3.生成相应的C语言的HelloWorld.h头文件,javah HelloWorld,头文件内容如下:(其中,static Java方法和非static Java方法经过javah -jni生成的C头文件是有区别的,差异在于参数,static Java方法生成的相应的C方法的第二个参数是jclass类型,非static Java方法生成的相应的C方法的第二个参数是jobject类型。)
more HelloWorld.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */
#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorld
* Method: hello
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_HelloWorld_hello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
4.开始编写第一个c语言的JNI的本地(native)方法:
vi HelloWorld.c
#include <jni.h>
#include "HelloWorld.h"
#include <stdio.h>
JNIEXPORT jint JNICALL Java_HelloWorld_hello
(JNIEnv* env, jobject target)
{
printf("Hello World in C program!!!\n");
return 2;
}
5.生成Unix下的动态库.so文件:
cc -G -I. -I$JAVA_HOME/include -I$JAVA_HOME/include/solaris HelloWorld.c -o libHelloWorld.so
在当前目录下看到新生成的libHelloWorld.so文件。也许你会注意到我们在Java程序中System.loadLibrary()加载的是库HelloWorld,那么这其实是一个命名的规范而已。以lib开头的.so文件即是Unix下的动态库,而JNI加载器通过HelloWorld这个名字实际上希望加载的动态库是libHelloWorld.so。
6.设置环境变量,查看LD_LIBRARY_PATH并设置:
vi .cshrc
setenv LD_LIBRARY_PATH '.:/usr/local/lib:$LD_LIBRARY_PATH'
source .cshrc
这个环境变量设置的是Java的动态库加载器需要加载的C动态库所在的路径。因此,我们生成的libHelloWorld.so一定要在这个环境变量中设置,在这个例子中我就把当前目录.加到环境变量LD_LIBRARY_PATH了:)
7.执行HelloWorld文件,java HelloWorld,在标准输出上看到:
Starting to load HelloWorld lib.
Load successfully.
Hello World in C program!!!
Say in Java:2
一切Okey,如此的easy,一阵狂喜!接下来就是将这个例子运用于JNI的开发之中了。
Resource:
《Java Native Interface: Programmer's Guide and Specification》
http://java.sun.com/docs/books/jni/html/start.html#27008
http://linuxmafia.com/faq/Admin/ld-lib-path.html
https://www6.software.ibm.com/developerworks/cn/education/java/j-jni/tutorial/j-jni-2-15.html -
2008-03-31
Java NIO TCP编程 - [Java]
在Java1.4以前,Java的网络编程是只有阻塞方式的,在Java1.4以及之后,Java提供了非阻塞的网络编程API.从Java的发展来看,由于Java的快速发展,JVM性能的提升,涉足到服务端应用程序开发也越来越多,要求高性能的网络应用越来越多,这是Java推出非阻塞网络编程的最主要原因吧。
对我而言,以前的大部分服务端应用主要是搭建在应用服务器之上,所以通讯这部分工作都是有应用服务器来实现和管理的。这次由于通讯和协议,我们必须自己实现一个能处理大量并发客户端的高性能并行处理的Java服务端程序。因此,选择非阻塞的处理方式也是必然的。我们首先来看看阻塞的处理方式:
在阻塞的网络编程方式中,针对于每一个单独的网络连接,都必须有一个线程对应的绑定该网络连接,进行网络字节流的处理。下面是一段代码:
public static void main(String[] args) {
try {
ServerSocket ssc = new ServerSocket(23456);
while (true) {
System.out.println("Enter Accept:");
Socket s = ssc.accept();
try {
(new Thread(new Worker(s))).start();
} catch (Exception e) {
// TODO
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static class Worker implements Runnable {
private Socket s;
private boolean running = true;;
public Worker(Socket s) {
this.s = s;
}
public void run() {
try {
InputStream is = s.getInputStream();
OutputStream os = s.getOutputStream();
while (running) {
byte[] b = this.readByLength(is, 1024);
this.process(b);
}
} catch (Throwable t) {
// TODO
t.printStackTrace();
}
}
private byte[] readByLength(InputStream is, int contLen) throws IOException {
byte[] b = new byte[contLen];
int off = 0;
int length = 0;
while ((length = is.read(b, off, contLen - off)) >= 0) {
off = +length;
if (off >= contLen) {
break;
}
}
return b;
}
private void process(byte[] b) {
}
}
在这段代码中,我们看到有两个阻塞的方法,是ServerSocket的accept()方法;和InputStream的read()方式。因此我们需要两类型的线程分别进行处理。而且每一个阻塞方法所绑定的线程的生命周期和网络连接的生命周期是一致的。基于以上的原因,NIO应运而生,一方面,为每一个网络连接建立一个线程对应,同时每一个线程有大量的线程处于读写以外的空闲状态,因此希望降低线程的数量,降低每个空闲状态,提高单个线程的运行执行效率,实际上是在更加充分运用CPU的计算、运行能力(因为,如果有大量的链路存在,就存在大量的线程,而大量的线程都阻塞在read()或者write()方法,同时CPU又需要来回频繁的在这些线程中间调度和切换,必然带来大量的系统调用和资源竞争.);另外一方面希望提高网络IO和硬盘IO操作的性能。在NIO主要出现了三个新特性:
1.数据缓冲处理(ByteBuffer):由于操作系统和应用程序数据通信的原始类型是byte,也是IO数据操作的基本单元,在NIO中,每一个基本的原生类型(boolean除外)都有Buffer的实现:CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer和ByteBuffer,数据缓冲使得在IO操作中能够连续的处理数据流。当前有两种ByteBuffer,一种是Direct ByteBuffer,另外一种是NonDirect ByteBuffer;ByteBuffer是普通的Java对象,遵循Java堆中对象存在的规则;而Direct ByteBuffer是native代码,它内存的分配不在Java的堆栈中,不受Java内存回收的影响,每一个Direct ByteBuffer都是直接分配的一块连续的内存空间,也是NIO提高性能的重要办法之一。另外数据缓冲有一个很重要的特点是,基于一个数据缓冲可以建立一个或者多个逻辑的视图缓冲(View Buffer).比方说,通过View Buffer,可以将一个Byte类型的Buffer换作Int类型的缓冲;或者一个大的缓冲转作很多小的Buffer。之所以称为View Buffer是因为这个转换仅仅是逻辑上,在物理上并没有创建新的Buffer。这为我们操作Buffer带来诸多方便。
2.异步通道(Channel):Channel是一个与操作系统紧密结合的本地代码较多的对象。通过Channel来实现网络编程的非阻塞操作,同时也是其与ByteBuffer、Socket有效结合充分利用非阻塞、ByteBuffer的特性的。在后面我们会看到具体的SocketChannel的用法。
3.有条件的选择(Readiness Selection):大多数操作系统都有支持有条件选择准备就绪IO通道的API,即能够保证一个线程同时有效管理多个IO通道。在NIO中,由Selector(维护注册进来的Channel和这些Channel的状态)、SelectableChannel(能被Selector管理的Channel)和SelectionKey(SelectionKey标识Selector和SelectableChannel之间的映射关系,一旦一个Channel注册到Selector中,就会返回一个SelectionKey对象。SelectionKey保存了两类状态:对应的Channel注册了哪些操作;对应的Channel的那些操作已经准备好了,可以进行相应的数据操作了)结合来实现这个功能的。
NIO非阻塞的典型编程模型如下:
private Selector selector = null;
private static final int BUF_LENGTH = 1024;
public void start() throws IOException {
if (selector != null) {
selector = Selector.open();
}
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket serverSocket = ssc.socket();
serverSocket.bind(new InetSocketAddress(80));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
try {
while (true) {
int nKeys = UnblockServer.this.selector.select();
if (nKeys > 0) {
Iterator it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
if (channel == null) {
continue;
}
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
readDataFromSocket(key);
}
it.remove();
}
}
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* @param key
* @throws IOException
*/
private void readDataFromSocket(SelectionKey key) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(BUF_LENGTH);
SocketChannel sc = (SocketChannel) key.channel();
int readBytes = 0;
int ret;
try {
while ((ret = sc.read(buf.buf())) > 0) {
readBytes += ret;
}
} finally {
buf.flip();
}
// process buffer
// buf.clear();
}
从这段程序,我们基本可以了解到NIO网络编程的一些特点,创建一个SocketServer的方式已经发生了变化,需要指定非阻塞模式,需要创建一个Channel然后注册到Selector中去,同样,建立一个网络连接过程也是一样的模式,然后就是有条件的选择(Readiness Selection).这样,我们的每一个线程只需要处理一类型的网络选择。在代码上,我们发现处理的方式和阻塞完全不一样了,我们需要完全重新考虑如何编写网络通信的模块了:
1.持久连接的超时问题(Timeout),因为API没有直接的支持timeout的参数设置功能,因此需要我们自己实现一个这样功能。
2.如何使用Selector,由于每一个Selector的处理能力是有限的,因此在大量链接和消息处理过程中,需要考虑如何使用多个Selector.
3.在非阻塞情况下,read和write都不在是阻塞的,因此需要考虑如何完整的读取到确定的消息;如何在确保在网络环境不是很好的情况下,一定将数据写进IO中。
4.如何应用ByteBuffer,本身大量创建ByteBuffer就是很耗资源的;如何有效的使用ByteBuffer?同时ByteBuffer的操作需要仔细考虑,因为有position()、mark()、limit()、capacity等方法。
5.由于每一个线程在处理网络连接的时候,面对的都是一系列的网络连接,需要考虑如何更好的使用、调度多线程。在对消息的处理上,也需要保证一定的顺序,比方说,登录消息最先到达,只有登录消息处理之后,才有可能去处理同一个链路上的其他类型的消息。
6.在网络编程中可能出现的内存泄漏问题。
在NIO的接入处理框架上,大约有两种并发线程:
1.Selector线程,每一个Selector单独占用一个线程,由于每一个Selector的处理能力是有限的,因此需要多个Selector并行工作。
2.对于每一条处于Ready状态的链路,需要线程对于相应的消息进行处理;对于这一类型的消息,需要并发线程共同工作进行处理。在这个过程中,不断可能需要消息的完整性;还要涉及到,每个链路上的消息可能有时序,因此在处理上,也可能要求相应的时序性。
当前社区的开源NIO框架实现有MINA、Grizzly、NIO framework、QuickServer、xSocket等,其中MINA和Grizzly最为活跃,而且代码的质量也很高。他们俩在实现的方法上也完全大不一样。(大部分Java的开源服务器都已经用NIO重写了网络部分。 )
不管是我们自己实现NIO的网络编程框架,还是基于MINA、Grizzly等这样的开源框架进行开发,都需要理解确定的了解NIO带来的益处和NIO编程需要解决的众多类型的问题。充足、有效的单元测试,是我们写好NIO代码的好助手:)
Resource:
http://www.cis.temple.edu/~ingargio/cis307/readings/unix4.html#states
《Java NIO》
《GlassFish--开源的Java EE应用服务器》 -
Java程序开发中有大量的可复用资源,可复用的公用类;有很多中间件已经帮助我们解决了多线程的问题,所以很多开发人员是不需要深入的涉及到这个话题的,而要自己去开发一个服务端的应用的时候,多线程的编程、调试、发布、问题跟踪就变得无可避免了。
多线程编程需要涉及到两个方向,一个是使用Java语言进行程序设计,必须了解语言本身对于线程的支持和其编程模型;另外一个是运行时的运行状况,更多的是基于现有的硬件、先有的操作系统的配置问题。所有的应用都必须运行一定的硬件配置环境、一定的操作系统之上,所以线程的运行时状态无可避免的要在应用设计阶段进行考虑了。
1.对于共享可变数据的互斥(什么时候使用关键字synchronized):Java虚拟机通过对象锁来实现互斥,达到多个线程在同一个共享数据上独立而互不干扰地工作。
private int sequence = 0;
public synchronized int getSequence(){
return sequence++;
}
线程间通讯,保证共享对象对于多线程访问是从一种一致的状态跃迁到另外一种一致的状态。
public class Worker implements Runnable{
private boolean running = false;
public void run(){
while(isRunning()){
//
}
}
public synchronized boolean isRunning(){
return this.running;
}
public synchronized void stop(){
this.running = false;
}
public synchronized void startRequest(){
this.running = true;
}
}
2.协作(什么时候使用wait和notify):主要是用于多线程为了同一个目标而共同协作性的工作,应用需要设计多线程的协作工作机制。一般性地是,线程需要反复检查某一个数据结构,在等待某些条件的发生,又避免忙等(busy-wait)的时候。
使用wait()是需要仔细阅读以下javadoc了,wait(0)的情况很特殊:线程一直处于等待,直到被唤醒(notify()或者notifyAll())。
private Object monitor = new Object();
public void run(){
synchronized(monitor){
while(){
try{
obj.wait()
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Java使用的同步机制是监视器,为了更好的使用同步机制,就需要虚拟机是怎么使用监视器的机制的?
Java监视器主要分成了三个区域,入口区、监视区、等待区,所有进入到监视器开始处的线程首先都是进入到入口去,(可能阻塞)等待成为监视区的持有者;监视区域同时只能被一个线程持有并执行,只有当一个线程执行完了监视区域动作(该线程将释放监视区域并推出监视器)或者执行了等待命令(该线程将释放监视区域进入到等待区,直到执行了唤醒命令才能重新持有监视区域)才能释放监视区域;只有那些持有监视区域的线程执行了等待命令之后就会进入到等待区。所以基本可以了解了监视区域的各个功能、线程进出的时机动作。而且分析互斥和协作情况下线程和监视器的工作状态基本也就清晰了。
1.互斥情况下,多线程将会在等待区阻塞的等待持有监视区域,因为同时只会有一个线程执行监视区域(代码即指令),监视区域主要是共享数据或者共享资源。(例外就是虚拟机实现不是基于时间片的,那么监视器就会用来协调多线程的执行策略,将不仅仅是共享数据或者共享资源了。)
2.协作情况下,监视器主要协调多个线程之间共同工作,即是,一个已经持有监视区域的线程,通过执行等待命令,释放监视区域进入到等待区,那么该线程阻塞并一直持续暂停状态,只有监视区域持有线程执行了唤醒该线程命令并释放监视区域之后该线程才能重新持有监视区域,直到该线程再次释放监视区域。
运行阶段,Java线程的线程模型又是什么样的呢,究竟需要应用设计开发人员需要注意什么呢?
这主要涉及到Java的内存模型(Memory Model)、JVM实现等,其中我正在使用的虚拟机是Sun基于Solaris系统的,其线程模型参考《Threading》。Java的线程在Java2都是操作系统的线程,并没有了"green threads"的概念了,每一个线程的申请在向JVM申请资源的同时也是在向操作系统申请资源,对于各家JVM在不同的操作系统之上的thread stack size大小也有一些差别,其中Sun JVM可以通过-XX:ThreadStackSize(-Xss)参数进行设置(在默认情况下,"Thread Stack Size (in Kbytes). (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]").
在我们设计并发要求比较高的服务端应用的时候,线程将会变得相当宝贵了,而且就针对操作系统而言,一个进程所能申请的线程的数量也是限制的,所以如何使用和调度多线程是一个重要的环节。在系统运行阶段,进行调优的时候不可避免的要面对多线程的资源相关的几个关注点:thread stack size、thread local heap、garbage collection affects、intimate shared memory。
进行多线程的设计编码,要极力追求简洁原则,尽力将多线程的调度和编码和业务相关的逻辑进行解耦,这样多线程处理模块被抽象出来,可复用度变高,对于分析、调试、问题跟踪多线程的设计编码就不再变得臃肿复杂;避免复杂、过多的条件判断等待、wait-notify,线程意外退出或者形成死锁。
在今天的编程生活中,我们也要面对和解决多线程编程的调试、单元测试、跟踪等方面的问题。解决这些问题也是我们的乐趣之一:)
参考:
《Effective Java 中文版》
《深入Java虚拟机》
http://java.sun.com/docs/hotspot/threads/threads.html
http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp
http://www.theserverside.com/tt/knowledgecenter/knowledgecenter.tss?l=ConcurrencyTestingJavaApps -
五一假期,老大结婚,一帮兄弟从各地感到郑州,一路旅行,很多朋友、同学很久没有见面,大家畅所欲言,把酒言欢,嬉笑怒骂,十分尽兴。旅途中重读了《The Secrets of Consulting》许多章节,书中很多做事的方法和原则,值得思考和品味。
1.“和我一样,维吉尼亚也很喜欢比喻,并且热衷于从各种地方--比如弗兰克.鲍姆的《绿野仙踪》--收集这些比喻。” 比喻是一种很好的表达的手段,通过类比和联想是很容易使人基于已知的世界去认识未知的世界的,而且往往很是深刻的。在这本书中看到作者是善于运用比喻的, 收集了很多比喻的。作者的工具箱中的所有物体都是现实世界中的实物,确能够被作者引申来说明一系列抽象的原则、远离、方法。
2. 《The Secrets of Consulting》中作者多次运用萨特的三个通用问题来分析问题的:
把问题分解成三个部分:自己、他人以及背景环境。在把自己作为研究焦点时,
- 我怎样到达这里的?(过去)
- 我在这里感觉如何?(现在)
- 我希望发生什么?(将来)
-
粗粗的读完《金字塔原理-----思考、写作与制作图表的逻辑》第一个部分。第一部分分为五章,主要介绍的是:写作的逻辑。如同本文所要讲述的思想一样,第一部分显得的简单而有强烈的逻辑关系。首先简述为什么要选择金字塔结构,是人类思考方式的局限性、一般性,思维的一般惯性:自上而下组织思想;自下而上思考。然后讲叙了金字塔结构的一般原则:疑问/回答式对话的纵向关系结构、演绎或者归纳逻辑的横向关系结构、讲故事式的序言结构。接着讲述了构造金字塔结构的一般方法:自上而下法;自下而上法;注意事项。既然方法已经讲叙了,所谓“万事开头难”,切题即如何写序言变得异常重要,接下来的一章就是关注序言部分的具体写法。介绍了为什么、是什么、怎么做、怎么开始,最后一章介绍了演绎和归纳的区别,意在突出如何将一个一个的思想“点”联系在一些形成逻辑关系,即是演绎和归纳。
看完也有一丝丝小学学语文的感觉,其实本身就是一个方法论的问题。
读完第一章节之后,我有一种模糊的感觉,写作和程序设计之间似乎有很多很多相通的地方,一篇能将思想简单而清楚的讲出来的文章才是好文章,程序设计更是需要简单清晰的。无论写作,还是程序设计,都需要搞清楚问题,搞清楚复杂性,搞清楚如何简单化等等。
以下是摘抄或者评注:
一个人的思维不能仅仅是线性的、一维的。
读者必然会将所读到的思想进行归类概括,以便记住这些思想。如果作者传达给读者的思想事先已经经过归类和概括,并且按自上而下的顺序表述出来,读者就能更容易的理解作者所表述的思想。这就是所说的“条理清楚”.
“因为你总是要不断地对思想进行归类和概括,直到没有可与之关联的思想可继续概括,因此,你写的每一篇文章的结构都必定只支持一个思想,即概括了所有各组思想的单一思想。”
“大多数人刚坐下来开始写作时,可能对他们想要表达的思想还只有一个模糊的想法,甚至根本不知如何下笔。在你不得不用话语或文字将你的思想用符号表示出来之前,你很可能无法准确地了解自己的思想。甚至连你认为已经构思好的第一个思想,写出来可能都不是十分准确。”
“为什么我们可以肯定一定会感兴趣?因为这种纵向联系迫使读者按照你的思想做出符合逻辑的反应。”
自上而下组织思想
自下而上思考
金字塔的子结构
纵向关系:不断地按照“引起读者疑问并回答疑问”的模式,一环扣一环如同波浪,直到读者不会再对新的表述提出任何疑问为止。
横向关系:保证一定的逻辑性,具备明确的归纳或者演绎关系。
序言结构:问题的起源和发展必然以讲故事的形式出现,因此也应当按照典型的讲故事模式发展。就是说,开头应向读者说明“情境(Situation)”的时间和地点。在这一“情境”中应当发展了某件事情称为“冲突(Complication)”,使读者提出(或将使读者提出)你的文章中将要回答 (Answer)的“疑问(Question)”。
作者在谈论“初学者注意事项”,所讨论的一些问题也是平时思维、做事等的一些通病,让人能觉察不仅仅是在写作上。
“文如其人”,写出来的东西完全是一个人思想的映像,写出来的东西透出的是一个人如何思考的。






