分类目录归档:java

Java Time(一)

在讨论java8的time前,先回顾一下java8以前的日期,主要两个类java.util包下的Date、Calendar,其次是java.sql包下面对java.util.Date的扩展类Date、Time、Timestamp。
下面就来吐槽一下以前的日期API:
1,Date计算、格式化麻烦;
2,非线程安全
3,时区处理麻烦;
吐槽完了,来看下以前处理日期的一些实例:
1,获取指定的时间,比如获取 2015-08-23 22:00:00 的时间:

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            Date date = sdf.parse("2015-08-23 22:00:00");
        } catch (ParseException e) {
            e.printStackTrace();
        }

(这里使用SimpleDateFormat还有个问题,SimpleDateFormat也是非线程安全的,假如说运行过程中其他线程修改了SimpleDateFormat的值,那悲剧就发生了。)
或者:

 Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR,2015);
        calendar.set(Calendar.MONTH,7);
        calendar.set(Calendar.DAY_OF_MONTH,23);
        calendar.set(Calendar.HOUR_OF_DAY,22);
        calendar.set(Calendar.MINUTE,0);
        calendar.set(Calendar.SECOND,0);
        Date time = calendar.getTime();

.
这里还要注意月份是从0开始的;

2,所有日期类都可变的,非线程安全的,简单说下:
一般的bean类都会有个getter和setter方法,修改date用set方法,getter方法只用做读取,但是由于Date是可变的,getter方法也能用于修改:

bean.getDate().setTime(1440342793161L);

这显然违反了我们的初衷,也容易出问题。

3,关于时区问题:
java.util.Date是没有时区概念的,时区一般是由SimpleDateFormat这个类来控制的,

            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
            Date date = sdf.parse("2015-08-23 11:00:00");//得到美国时间 date
            System.out.println(sdf.format(new Date()));//格式化为美国的当前时间

或者是Calendar:

        Calendar calendar = Calendar.getInstance();
        calendar.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        System.out.println(calendar.get(Calendar.HOUR_OF_DAY));

所以,java以前的时间经常被吐槽,所以一般我们都会写一个DateUtil来处理,或者用第三方的库比如说apache commons的DateUtils;因此后来joda解决了以前java日期各种问题,再后来官方的java8也加入了datetime包解决旧版本时间的各种问题

Java Unsafe类实现任意实例浅克隆

了解java并发包或nio底层的都应该知道Unsafe这个类,如并发包的锁,通过Unsafe#park() 和Unsafe#unPark()来实现线程阻塞和恢复运行的,这个类没有公布源码,但是有很多比较有用的方法,它可以直接操作内存,使用的时候务必要谨慎,不小心可能会造成内存泄漏。

实现浅克隆思路
为了表述方便,用S代表要克隆的对象,D表示克隆后的对象,SD表示S的内存地址,DD表示D的内存地址,SIZE表示该对象在内存中的大小。

1,获取原对象的所在的内存地址SD
2,计算原对象在内存中的大小SIZE
3,新分配一块内存,大小为原对象大小SIZE,记录新分配内存的地址DD。
4,从原对象内存地址SD处复制大小为SIZE的内存,复制到DD处
5,DD处的SIZE大小的内存就是原对象的浅克隆对象,强制转换为源对象类型就可以了

unsafe方法介绍
unsafe有很多比较牛逼的方法,刚开始接触可能会感到不可思议,下面介绍几个比较实用的方法。
1,Object allocateInstance(Class aClass)
这个方法是分配一个实例,它的牛逼之处在于,只要是非abstract的类它都能实例化,即使这个类没有public的构造方法,它甚至能绕过各种JVM安全检查,不运行任何构造方法,当你用这个方法初始化类后,通过getClassLoader()方法视图去获取他的classloader会发现它返回的是null。是不是很神奇
2,long objectFieldOffset(Field f)
获取对象属性偏移量
3,int arrayBaseOffset(Class arrayClass)
这个方法是返回一个数组第一个元素的偏移量,这个可以用在获取对象内存地址的时候,因为unsafe没有提供直接获取对象实例内存地址的方法,只有获取通过对象属性偏移量获取属性内存地址的方法,所以我们可以通过构建一个数组对象,通过数组元素偏移量获取元素的内存地址,可以参考后面的代码。
4,long allocateMemory(long bytes)
这个方法是直接分配一个bytes大小的内存,然后返回内存的起始地址。
5,long getLong(Object o, long offset)
获取对象o的偏移量为offset位置的long值,这个long值是该位置后64位二进制的long值,同样的方法还有getInt,getByte等
6, putLong(Object o, long offset, long x);
把long x的2进制值放在对象o的offset偏移量的位置。

offset偏移量:是只相对于另一个地址便宜的byte数;

获取Unsafe实例
Unsafe这个类是在sun.misc包下,构造器是私有的,提供一个getUnsafe()的方法获取单例,仅供java类库的类使用,即只能是BootstrapClassLoader加载器加载的类使用,源码如下:

    private Unsafe() {}

    private static final Unsafe theUnsafe = new Unsafe();

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class cc = Reflection.getCallerClass();
        if (cc.getClassLoader() != null)
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }

可以通过反射的方式获取它的实例:

        Field theUnsafe = null;
        try {
            theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

1,获取原对象的内存地址

unsafe类没有提供直接获取实例对象内存地址的方法,但是可以间接获取,大概思路是:构建一个新对象N,N包含了S对象的引用,只要获取到N对S的引用地址就是S的内存地址了;
unsafe类获取内存地址的方法只有一个,就是getLong(Object o, long offset);为了方便,可以构建一个包含S的Object[]数组,获取到Object[]的S引用就可以了,代码如下:

    private static long getAddr(Object obj) {
        Object[] array = new Object[]{obj};
        long baseOffset = unsafe.arrayBaseOffset(Object[].class);
        return unsafe.getLong(array, baseOffset);
    }

2,计算原对象在内存中的大小
我们新建一个对象实例后,jvm做的其实只是在堆中分配非static的Field的内存,其他的static属性,或者方法在加载期间就已经放到内存中去了,所以当我们计算对象大小时只要计算field的大小就行了,jvm分配内存时单个实例中的每个field内存都是连续的,所以我们只需要获得最大偏移量的Field的偏移量,然后加上这个field的大小就可以了,代码如下:

    public static long sizeOf(Class<?> clazz) {
        long maximumOffset = 0;
        Class<?> maxiNumFieldClass = null;
        do {
            for (Field f : clazz.getDeclaredFields()) {
                if (!Modifier.isStatic(f.getModifiers())) {
                    long tmp = unsafe.objectFieldOffset(f);
                    if(tmp>maximumOffset){
                        maximumOffset = unsafe.objectFieldOffset(f);
                        maxiNumFieldClass = f.getType();
                    }
                }
            }
        } while ((clazz = clazz.getSuperclass()) != null);
        long last = byte.class.equals(maxiNumFieldClass)?1:
                ( short.class.equals(maxiNumFieldClass) || char.class.equals(maxiNumFieldClass))?2:
                        (long.class.equals(maxiNumFieldClass)||double.class.equals(maxiNumFieldClass))?8:4;
        return maximumOffset + last;
    }

3,新分配一块大小为SIZE的内存
Unsafe提供了方法 long allocateMemory(long bytes); 可直接分配一块内存,返回内存地址。

4,从原对象内存地址SD处复制大小为SIZE的内存到DD位置
见Unsafe方法:void copyMemory(long srcAddress, long destAddress, long bytes)

5,DD处的SIZE大小的内存赋值给目标对象
Unsafe没有提供直接读内存转为java对象的方法,但是可以通过类似获取对象内存地址的方法来实现:
先新建一个包含S类型属性的对象,让后把DD的内存地址赋值给S类型的属性变量就可以了:

    private static <T> T fromAddress(long addr, long size) {
        Object[] array = new Object[]{null};
        long baseOffset = unsafe.arrayBaseOffset(Object[].class);
        unsafe.putLong(array, baseOffset, addr);
        return (T) array[0];
    }

克隆方法
如果是数组,这个方法就不适用了,因为数组比较特殊,数组类是jvm在运行时动态生成的,有兴趣可以去研究下jvm对数组的处理。最终的克隆方法代码如下:

    public static <T> T shallowClone(T t) throws InstantiationException {
        Class clazz = t.getClass();
        if(clazz.isArray()){
            Object[] os = (Object[])t;
            return (T)Arrays.copyOf(os,os.length);
        }
        long srcAddr = getAddr(t);
        long size = sizeOf(clazz);
        long destAddr = unsafe.allocateMemory(size);
        unsafe.copyMemory(srcAddr, destAddr, size);
        return fromAddress(destAddr, size);
    }

测试

        Object s = new Foo(8,888L,"test")
        Object s2 = shallowClone(s);
        Assert.assertEquals(s, s2);
        Assert.assertTrue(s != s2);

ps:用Unsafe分配的内存不在jvm管理的范围内,所以,jvm不会自动去回收这一块内存,你得通过Unsafe#freeMemory(long address) 去释放这块的内存。

javamail接收时邮件标题乱码解决方案

通过javamail接收邮件时,获取到Message后,通过Message#getSubject()获取邮件标题时出现中文乱码,如:[�求�� 5068761788:�I家已收到退款],这种情况只有在对方标题内容有繁体字,而且使用的编码是GB2312时就出现,把编码改为GBK后显示正常,这种情况是GB2312没有收录这部分繁体字符而无法显示导致的。

GB2312(1980年)共7445个字符,GBK(1995年)共21886个字符,GBK字符编码向上兼容GB2312,这部分无法显示的繁体字符在GBK收录的范围,因此GBK能正常显示。

查看Email的源码可以看到类似 Subject: =?GB2312?B?1tA=?= 的字符,其中第一个 ?GB2312?是指字符编码,第二个?B?,表示内容的编码方式,如果是B则表示Base64编码,如果是“Q”则代表 Quoted-Printable编码。

javamail在Message#getSubject()的方法中,通过MimeUtility#decodeText()对邮件消息的标题进行解码 ,通过MimeUtility#javaCharset(String charset)这个方法获取真正的字符集编码,代码如下:


public static String javaCharset(String charset) {
 if (mime2java == null || charset == null)
 // no mapping table, or charset parameter is null
 return charset;

String alias =
 (String)mime2java.get(charset.toLowerCase(Locale.ENGLISH));
 return alias == null ? charset : alias;
 }

可以看到最终会通过mime2java这个对象获取编码,mimi2java是一个Hashtable对象,在类加载的时候进行初始化,代码如下:


InputStream is =
 javax.mail.internet.MimeUtility.class.getResourceAsStream(
 "/META-INF/javamail.charset.map");

if (is != null) {
 try {
 is = new LineInputStream(is);

// Load the JDK-to-MIME charset mapping table
 loadMappings((LineInputStream)is, java2mime);

// Load the MIME-to-JDK charset mapping table
 loadMappings((LineInputStream)is, mime2java);

if (java2mime.isEmpty()) {

...

}

if (mime2java.isEmpty()) {

mime2java.put("iso-2022-cn", "ISO2022CN");

......

}

程序会先在”/META-INF/javamail.charset.map”这个文件中加载字符的映射关系,如果没有则添加默认值。到这里已经找到问题的解决办法了。

在项目下添加”/META-INF/javamail.charset.map”这个文件,文件内容为默认的映射关系+GB2312->GBK的映射关系,让程序通过GBK去解码全部GB2312的内容:


---------mimi2java(required)--------------
iso-2022-cn ISO2022CN
iso-2022-kr ISO2022KR
utf-8 UTF8
utf8 UTF8
ja_jp.iso2022-7 ISO2022JP
ja_jp.eucjp EUCJIS
euc-kr KSC5601
euckr KSC5601
us-ascii ISO-8859-1
x-us-ascii ISO-8859-1
gb2312 GBK

ps:这里第一行带有“——“的内容必须有,否则系统会把这些内容赋值给java2mime对象,而不对赋值给mime2java对象,程序是通过”–”标识或者空行来分割的.

每行内容必须用一个tab分开,因为javamail是通过”\t”来分割key和value的.源代码如下:


while (true) {

... ...

if (currLine == null) // end of file, stop
 break;
 if (currLine.startsWith("--") && currLine.endsWith("--"))
 // end of this table
 break;

... ...

StringTokenizer tk = new StringTokenizer(currLine, " \t");

... ...

}

javamail通过代理服务器发送邮件

这段时间在写一个邮件系统,因为可能有很多帐号,需要使用不同的代理服务器发送,在网上查了很多资料,很多demo基本上都是没用,真不知道哪些家伙刷那么多垃圾帖干嘛,自己都没测试就贴出来,浪费老子时间,真想扇他们几个耳刮子。

不想浪费在这海量垃圾资料的查阅中,只能自己动手丰衣足食了。通过跟了大半天的源码终于成功了。

javamail只有1.4.5及以上版本才支持代理发送,而且支持socks代理,http代理不支持,网上有很多类似的代码如:


System.getProperties().put("http.proxySet","true");
System.getProperties().put("http.proxyHost","127.0.0.1");
System.getProperties().put("http.proxyPort","8098");

这些我尝试都不成功,JAVAMAIL API FAQ中也有提到:Note that setting these properties directs all TCP sockets to the SOCKS proxy, which may have negative impact on other aspects of your application.这个会代理所有的tcp socket,可能会影响到其他程序,而且不能做得每个帐号不同的代理,也会有并发的问题,显然这不是我想要的。

JAVAMAIL API FAQ提到了通过设置:mail.smtp.socks.host属性来做到session级别的代理,但是经过尝试还是没有成功,代码如下:


properties.put("mail.smtp.socks.host","127.0.0.1");
properties.put("mail.smtp.socks.port","8098");

通过一步一步代码跟踪,发现在 com.sun.mail.util.SocketFetcher#createSocket(InetAddress localaddr, int localport,String host, int port, int cto, int to,Properties props, String prefix,SocketFactory sf, boolean useSSL) 这个方法中开始出现了使用socks的代码:


String socksHost = props.getProperty(prefix + ".socks.host", null);

调用这个方法的代码:


serverSocket = SocketFetcher.getSocket(host, port,
props, "mail." + name, isSSL);

可以知道,prefix为”mail.”+name

name在SMTPSSLTransport的构造方法中定义,值为“smtps”,代码如下:


public SMTPSSLTransport(Session session, URLName urlname) {
 super(session, urlname, "smtps", true);
 }

这里调用SMTPSSLTransport的构造方法是由配置:

 properties.put("mail.transport.protocol", "smtps");

来决定的;

在session初始化的时候:

mailSession = Session.getInstance(properties);

会调用 Session#loadProviders 方法


addProvider(new Provider(Provider.Type.TRANSPORT,
 "smtps", "com.sun.mail.smtp.SMTPSSLTransport",
 "Sun Microsystems, Inc.", Version.version));

把相应的Provider初始化好,在获取Transport的时候会根据配置获取相应的Provider, Session#getTransport:

getTransport(getProperty("mail.transport.protocol"))

因此这里的设置代理的时候name为stmps而不是stmp:

properties.put("mail.smtps.socks.host","127.0.0.1");
properties.put("mail.smtps.socks.port","8098");

这样设置后虽然能正确获取host和端口了,但是最终还是没有使用代理,继续跟踪发现socket在这里创建:


if (sf != null)
 socket = sf.createSocket();
if (socket == null) {
 ......
 // get & invoke the getSocket(host, port) method
 Method mthGetSocket = proxySupport.getMethod("getSocket",
 new Class[] { String.class, int.class });
 socket = (Socket)mthGetSocket.invoke(new Object(),
 ......
 }

这个sf即配置的socketFactory,具体代码在方法:SocketFetcher#getSocket(String host, int port, Properties props,String prefix, boolean useSSL) 中,

因此只有不配置socketFactory就会使用配置的代理去生成socket;

于是乎把socketFactory相关配置,如:

 properties.put("mail.imaps.ssl.socketFactory.class", "javax.net.ssl.SSLSocketFactory");

去掉,再运行,完美运行。

关于测试是否使用了代理发送,在收到邮件后,在foxmail中右键邮件->更多操作->查看邮件源码:


Return-Path: <xxxx@gmail.com>
Received: from xxxx  ([my proxy ip])

这里my proxy ip就是代理的ip了;

 

 

 

Java 8 lambda表达式

jdk8最重要的特性-lambda表达式,意味着java可以函数式编程那样直接定义一个函数了,因此java8先从学习ambda开始。先看熟悉的例子,新建一个线程:

new Thread(new Runnable() {
@Override
public void run() {
System.out.println("this is thread run method");
}
});

用lambda表达式的代码:


new Thread(() -> System.out.println("this is thread run method")).start()

上面“ () -> System.out.println(“this is thread run method”) ”这部分就是lambda表达式了。
这一行代码运行了一个新建的线程,并用lambda表达式传递了一个 System.out.println(“this is thread run method”)的run方法体,相比以前代码简洁了不少。

Java8加入lambda表达式后,意味着方法可以进行传递了,Java也能像python的filter,map,reduce这样进行玩耍了。

在介绍lambda表达式之前,我们先得知道java8的函数接口:

JSR335中对函数式接口的描述是:

functional interface is an interface that has just one abstract method (aside from the methods of Object), and thus represents a single function contract. (In some cases, this “single” method may take the form of multiple abstract methods with override-equivalent signatures inherited from superinterfaces; in this case, the inherited methods logically represent a single method.)

函数接口是声明有且只有一个抽象方法的接口,这个抽象方法也可以是继承其他接口的,函数接口也可以有java8新增的default方法。上面这段代码用lambda表达式替换了继承Runnable接口的匿名类,这里Runnable就是所谓的函数接口了。在jdk8新增了一个函数接口的注解“ @FunctionalInterface” ,加上这个注解后,该接口就必须符合函数接口的规范,否则无法编译。

申明了函数接口后,我们就可以通过lambda表达式来实现这个接口的方法及这个接口的调用了。

lambda的申明有两部分组成,分别为参数和方法体,由“->”隔开,
参数部分:当没有参数时,我们用”()”表示,有参数时,我们在括号内申明参数,如”(int arg1,String arg2)”;编译器可以对参数类型自动推导,因此我们还可以写成”(arg1,arg2)”,如果只有一个参数,甚至可以去掉”()”,直接写成”arg1″;

方法体部分:我们可以用”{}”来表示,当该函数接口有返回值的时候我们可以这么表示{return value;} ,当然我们也可以直接写成“value”,如果有函数接口的方法有返回值则会自动返回value。

例:

函数接口:


@FunctionalInterface
interface Addable{
 int add(int i);
}

调用接口方法:

public void testAdd(Addable fn,Integer num){
System.out.println(fn.add(num));
 }

因此我们可以这么调用testAdd方法:

testAdd((int i)->{return i+100;},88);

我们可以把类型定义去掉,方法体只有一句代码我们也可以把“{}”去掉,同时把return去掉,变成了下面的代码:

testAdd((i) -> i + 100,88);

由于参数只有一个,我们可以把参数部分的”()”都去掉,最简洁的代码是:

testAdd(i -> i + 100,88);

以上的运行结果当然都是188。

以后也可以这样引用一个lambda,代码:


Comparator<String> compareLength = (a,b)->a.length()-b.length();

lambda表达式我们可以理解为就是实现了一个函数接口的匿名类的实例。
但是实际上java8并不是仅仅只在编译器上做个编译转换处理完事了,我们把上面的代码编译的class类的字节码扒出来看一下就知道了。
java代码:

public class LambdaTest {
 public static void main(String[] args) {
 new Thread(() -> System.out.println("hello lambda")).start();
 }
}

在命令行切换到class目录,运行命令:

javap -verbose LambdaTest.class

找到如下的打印的信息:

public static void main(java.lang.String[]);
 descriptor: ([Ljava/lang/String;)V
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=3, locals=1, args_size=1
 0: new #2 // class java/lang/Thread
 3: dup
 4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
 9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
 12: invokevirtual #5 // Method java/lang/Thread.start:()V
 15: return

可以看到“4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;”,这里有一个invokedynamic的指令。而且没有生成“Method com/sed/LambdaTest$1.”这种带$符号的匿名类。所以这里只是定义了一个方法调用点,只有在执行的时候才能知道具体调用的是哪个方法,这就是所谓的延迟执行。

了解了lamdba表达式,我们可以像Python一样使用filter,map,reduce类似的方法了。

在集合中,java8在Collection接口新增了一个stream()的default方法,可通过返回的Stream对象进行filter和forEach操作,代码:


List<String> list = Arrays.asList("aaa","bba","ccc");
 list.stream().filter(s->s.contains("a"))
              .forEach(s-> System.out.println(s+"_"));

运行结果如下:


aaa_

bba_

怎么样,java8的lambda是不是很酷!!!

java线程重排序问题

以前在看《java并发编程实战》的时候,看到重排序的时候始终不能很好理解,做了很多demo也未能实现书中所述效果,见CSDN上的提问

按作者的意思是,当线程A运行的时候,在没有同步或者使用锁的情况下,其他线程看到的线程A内的代码运行顺序可能被打乱。即后面的代码可能会先运行。

代码:

</pre>
public class NoVisibility {
private static boolean ready;
private static int     number;

private static class ReaderThread extends Thread {
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}

public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
<pre>

ReaderThread线程可能会先看到主线程中ready=true。但是经过多次实验均未看到此效果。 今天偶然看到关于系统架构的文章,觉得可能问题出在系统的架构差异导致的。

从系统架构来看,目前的商用服务器大体可以分为三类,即对称多处理器结构 (SMP : Symmetric Multi-Processor) ,非一致存储访问结构 (NUMA : Non-Uniform Memory Access) ,以及海量并行处理结构 (MPP : Massive Parallel Processing)  本人DEMO运行环境为intel I3处理器. 在SMP系统架构中,所有的CPU共享全部资源,如总线,内存和I/O系统等,操作系统或管理数据库的复本只有一个,这种系统有一个最大的特点就是共享所有资源。多个CPU之间没有区别,平等地访问内存、外设、一个操作系统。操作系统管理着一个队列,每个处理器依次处理队列中的进程。此外SMP架构还存在内存屏障,保障指令的顺序执行。

本人运行环境是在intel I3处理器下运行的,估计此款CPU采用的就是SMP架构。

而在NUMA架构中,不同CPU之间具有独立的本地内存、 I/O 槽口等,CPU之间的通讯是通过互联模块 ( 如称为 Crossbar Switch) 进行连接和信息交互,因此每个 CPU 可以访问整个系统的内存 ( 这是 NUMA 系统与 MPP 系统的重要差别 ),这将导致在不同CPU通讯的时候,读取数据的CPU没有读到写CPU的实际数据。

另外除了CPU对指令的乱序执行外,编译器在编译的时候也可能会对代码进行重排序。CPU在乱序执行中对指令的执行顺序是不会打乱的,而只是可能不会等到前面的指令执行完在执行后面的指令,如同并发执行,相比于CPU的乱序,编译器的乱序才是真正对指令顺序做了调整。但是编译器的乱序也必须保证程序上下文的因果关系不发生改变。

本文只做笔记,关于重排序更深的理解还需对JVM内存模型,系统架构等做更深入研究

参考: SMP、NUMA、MPP体系结构介绍

中央处理器

为什么要指令重排序和屏障的作用

JAVA学习笔记–4.多线程编程 part1.背景知识和内存模型

 

 

定长线程池newFixedThreadPool

在多线程“生产者-消费者”模式中,为了避免线程陷入竞争而耗尽资源,通常可以通过Executors.newFixedThreadPool(num)限制线程的数量来解决问题,但是如果“生产者”数量持续大于“消费者”数量问题就容易产生了:

    public static void main(String[] args) throws InterruptedException {
        int num = 10000000;
        ExecutorService es = Executors.newFixedThreadPool(2);
        for (int i = 0; i < num; i++) {
            es.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                    }
                }
            });
        }
        es.shutdown();
        System.out.println("main end...");
    }

这里创建了一千万个线程,限制线程池大小为2;为了让”消费者”消费速度小于生产速度更明显,让创建的线程休息1秒。本人运行环境机器是4G内存,jvm堆空间默认大小,运行后抛出OutOfMemoryError,这是因为线程创建线程后,但是由于消费速度小于生产速度,线程池满后线程虽然没有立即执行,而是放在了等待队列中,当持续消费速度小于生产速度时,等待队列则持续增长,最终把堆空间撑爆了。

解决这个问题可以配合信号变量使用,使线程池满后,生产者暂停生产,陷入阻塞状态:

public static void main(String[] args) throws InterruptedException {
        int num = 10000000;
        int threadNum = 2;
        ExecutorService es = Executors.newFixedThreadPool(threadNum);
        final Semaphore semaphore = new Semaphore(threadNum);    //创建与线程池大小相当的信号变量
        for (int i = 0; i < num; i++) {
            semaphore.acquire(); //线程池满后主线程将暂时阻塞
            es.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                    }finally{
                        semaphore.release(); //线程运行完后信号释放,主线程恢复运行
                    }
                }
            });
        }
        es.shutdown();
        System.out.println("main end...");
    }

如此,堆空间将不再担心资源枯竭,只是主线程会受到阻塞,只需另起一个线程代替主线程即可。

mybatis自动生成代码文件

下载文件:https://code.google.com/p/mybatis/downloads/list?can=1&q=Product%3DGenerator
下载后解压,里面包含文档,我下载的是1.3.2版本。
建立generator.xml:

<?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE generatorConfiguration  
      PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"  
      "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">  
      
    <generatorConfiguration>  
      <!-- classPathEntry:数据库的JDBC驱动的jar包地址-->  
      <classPathEntry location="D:\jarlib\jdbc\mysql-connector-java-5.1.27.jar" />  
    <context id="DB2Tables" targetRuntime="MyBatis3">  
      <commentGenerator>  
        <!-- 是否去除自动生成的注释 true:是 : false:否 -->  
        <property name="suppressAllComments" value="true" />  
        <!--数据库连接的信息:驱动类、连接地址、用户名、密码 -->  
      </commentGenerator>  
      <jdbcConnection driverClass="com.mysql.jdbc.Driver"  
              connectionURL="jdbc:mysql://localhost:3306/mydb"  
              userId="root"  
              password="root">  
      </jdbcConnection>  
        <!--  默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer   
             true,把JDBC DECIMAL 和 NUMERIC 类型解析为java.math.BigDecimal   
         -->   
      <javaTypeResolver >  
         <property name="forceBigDecimals" value="false" />  
      </javaTypeResolver>  
      <!-- targetProject:自动生成代码的位置 -->  
      <javaModelGenerator targetPackage="entity" targetProject="D:\workspace\jarvis\test\webtest\src">  
          <!-- enableSubPackages:是否让schema作为包的后缀 -->       
          <property name="enableSubPackages" value="true" />  
        <!-- 从数据库返回的值被清理前后的空格  -->   
          <property name="trimStrings" value="true" />  
      </javaModelGenerator>  
      <!-- mapper xml -->  
      <sqlMapGenerator targetPackage="conf"  targetProject="D:\workspace\jarvis\test\webtest\src">  
           <property name="enableSubPackages" value="false" />  
      </sqlMapGenerator>  
       <!-- mapper Interface --> 
      <javaClientGenerator type="XMLMAPPER" targetPackage="mapper"  targetProject="D:\workspace\jarvis\test\webtest\src">  
        <property name="enableSubPackages" value="true" />  
      </javaClientGenerator>  
      <!-- tableName:用于自动生成代码的数据库表;domainObjectName:对应于数据库表的javaBean类名 -->  
      <table schema="untodo" tableName="t_student" domainObjectName="Student" >
	   <!--<property name="useActualColumnNames" value="true"/>-->
      <generatedKey column="id" sqlStatement="MySql" identity="true" />
      <columnOverride column="student_num" property="studehtNum" />
     <!-- <ignoreColumn column="FRED" /> -->
     <!-- <columnOverride column="LONG_VARCHAR_FIELD" jdbcType="VARCHAR" />-->
	  </table>
      <table schema="untodo" tableName="t_teacher" domainObjectName="Teacher" >  </table>  
    </context>       
</generatorConfiguration>  

通过命令执行:java -jar mybatis-generator-core-1.3.1.jar -configfile D:\mybatistool\mybatis-generator-core-1.3.1\genrator.xml -overwrite

Java(Android)线程池【转】


介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用。本文是基础篇,后面会分享下线程池一些高级功能。
1、new Thread的弊端
执行一个异步任务你还只是如下new Thread吗?
[Java]
new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
}
}).start();[/java]
那你就out太多了,new Thread的弊端如下:
a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,Java提供的四种线程池的好处在于:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。

2、Java 线程池
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
(1). newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。示例代码如下:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    final int index = i;
    try {
        Thread.sleep(index * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
 
    cachedThreadPool.execute(new Runnable() {
 
        @Override
        public void run() {
            System.out.println(index);
        }
    });
}

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

(2). newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。示例代码如下:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    final int index = i;
    fixedThreadPool.execute(new Runnable() {
 
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    });
}

因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。
定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。

(3) newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
 
    @Override
    public void run() {
        System.out.println("delay 3 seconds");
    }
}, 3, TimeUnit.SECONDS);

表示延迟3秒执行。

定期执行示例代码如下:

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
 
    @Override
    public void run() {
        System.out.println("delay 1 seconds, and excute every 3 seconds");
    }
}, 1, 3, TimeUnit.SECONDS);

表示延迟1秒后每3秒执行一次。
ScheduledExecutorService比Timer更安全,功能更强大,后面会有一篇单独进行对比。

(4)、newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:


ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
    final int index = i;
    singleThreadExecutor.execute(new Runnable() {
 
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    });
}

结果依次输出,相当于顺序执行各个任务。
现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。

Data URI scheme

这几天都在抢票 ,在研究360抢票插件时发现验证码的src为一串非正常URL的字符“data:image/png;base64,… …”,于是对这串字符感到好奇而进行了学习。
这串字符叫做:Data URI scheme。Data URI scheme是在RFC2397中定义的,目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。
字符组成:
data:表示取得数据的协定名称
image/png:表示MIME TYPE数据类型.
base64:编码方式。
逗号后面的是编码后的字符。
目前支持的类型有:

data:,文本数据
data:text/plain,文本数据
data:text/html,HTML代码
data:text/html;base64,base64编码的HTML代码
data:text/css,CSS代码
data:text/css;base64,base64编码的CSS代码
data:text/javascript,Javascript代码
data:text/javascript;base64,base64编码的Javascript代码
data:image/gif;base64,base64编码的gif图片数据
data:image/png;base64,base64编码的png图片数据
data:image/jpeg;base64,base64编码的jpeg图片数据
data:image/x-icon;base64,base64编码的icon图片数据

在php中base64用如下编码方法即可:

base64_encode(file_get_contents('test.png'))

在java中用sun.misc.BASE64Encoder进行base64编码文件:

File file = new File("test.png");
FileInputStream fis = new FileInputStream(file);
byte[] buf = new byte[1024];
int len = 0;
int count = 0;
byte[] total = new byte[]{};
while((len=fis.read(buf))!=-1){
	count = total.length+len;
	byte[] tmp = new byte[count];
	System.arraycopy(total, 0, tmp, 0, total.length);
	System.arraycopy(buf, 0, tmp, total.length ,len);
}
fis.close();
BASE64Encoder be = new BASE64Encoder();
String string = be.encode(total);

使用data URI scheme与普通的http URI scheme区别如下:
1,浏览器对编码的数据不会进行缓存,并且随html同时加载,不另起请求;
2,便于传输,不会被拦截,只要页面能加载,便能获取到编码后的数据,这样360抢票插件就能将验证码图片进行编码后传输,在服务器后台进行识别,而不必去关心session问题;
3,保存数据的大小上受限制(Firefox 1.5支持高达100KB的数据);
4,编码后会增加图片大小;
3,但如果使用过多会增加html的大小,可用css解决,即在css的background-image中使用data URI scheme,如:

background-image: url("data:image/png;base64/9j/4AA....+b0//2Q==");

但是IE8以下版本不兼容。ie以下只得用http URI scheme.

参考:http URI scheme与data URI scheme

什么是data URI scheme及如何使用data URI scheme