synchronized 与高性能并发

自Java 1.5推出java.util.concurrent包以后,其中ReentrantLock以其高效的性能,灵活的控制权得到了大家的喜爱。相比笨重的synchronized则成为低效的代名词,被人逐渐放弃。直到今天依然有很多程序员认为Lock比synchroized性能更高。

实际上作为亲儿子,synchronized自Java 1.6后得到了大量的优化,引入了“锁消除”、“锁粗化”、“偏向锁”、“轻量级锁”、“适应性自旋”等技术减少锁操作的开销。

锁消除(Lock elision)

HotSpot通过逃逸分析检测到一个对象不存在外部竞争,会通过锁消除来节省无意义的锁请求时间,以提高性能。如:

public String hello() {
   StringBuffer sb = new StringBuffer();
   sb.append("Hello");
   sb.append("World");
   return sb.toString();
} 

在此代码中,HotSpot可以明显分析出sb对象并未逃逸出hello方法外,因此可放心大胆的将synchroized锁同步操作移除。

锁粗化(Lock coarsening)

某些情况下,我们需要循环多次调用如Vector.add StringBuffer.append等方法,若每次均申请、释放锁,无疑是很大的资源浪费。因此HotSpot若检测到对同一个对象连续的加解锁动作,会将其合并为一个更大的锁。如:

public void addStooges(Vector v) {
    v.add("Hello");
    v.add("word");
    v.add("!");
}

在此代码中,HotSpot会将锁的申请与释放粗化至整个方法体,一次完成锁的申请与释放。

偏向锁(Biased Locking)

锁存在4种状态,依次为:无锁、偏向锁、轻量级锁、重量级锁。随着竞争激烈逐渐升级,锁只可升级不可降级。

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。

获取锁

  1. 检测Mark Word(Java对象头,4字节32位)是否为可偏向状态,即是否为偏向锁1,锁标识位为01。
  2. 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3)。
  3. 如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4)。
  4. 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块。

释放锁

偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:

  1. 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态。
  2. 撤销偏向苏,恢复到无锁状态(01)或者轻量级锁的状态。

轻量级锁(Lightweight Locking)

引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

获取锁

  1. 判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3)。
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3)。
  3. 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

释放锁

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据。
  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3)。
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。
  4. 对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

重量级锁

重量级锁本质是依赖于底层操作系统的互斥锁实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

自旋与适应性自旋

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。

所谓自旋就是执行一段代码,避免进入挂起状态,看持有锁的线程是否会释放锁。最简单的自旋代码就像:

while (checkLock);

自旋锁于Java 1.4.2中引入,默认关闭,使用-XX:+UseSpinning开启。Java 1.6默认开启,并可使用-XX:preBlockSpin设置自旋次数,默认为10次。

虽然可以通过-XX:preBlockSpin设置自旋次数,但设置过大则将消耗过多的CPU,过小则可能刚退出即释放了锁。所以 Java 1.6新增加了适应性自旋。

所谓适应性自旋即自旋次数不固定,它是由同一个锁上一次自旋结果来决定。若上一次执行成功,则本次允许更多的自旋次数。反之若某锁很少会自旋成功,则会减少自旋次数甚至取消自旋,以节省CPU时间。

总结

在竞争不激烈的情况下,synchronized关键字可提供更高的性能。但若竞争激烈,ReentrantLock可提供更灵活的锁策略,实际项目中要根据实际情况进行选择。

使用 Nginx 构建优化的 https 服务

推荐使用 Nginx 1.6 以上版本,用以支持 spdy 3.0。虽然 http/2 已经推出,但现在浏览器支持度太低,服务器也只有几个小应用,Nginx 和 Apache 都还没有支持 http/2。

所以 spdy 依然是加速 https 的最好方式,但 Chrome 新版已经不支持 spdy 2.0 及以下版本,所以新版 Nginx 对 https 是很有帮助的。

首先监听443端口。

listen       443 ssl spdy;

老版本 Nginx 大家都用 ssl on,新版本(0.7.14以上)就推荐在 listen 加 ssl 关键字,这样最大的好处是在一个 Server 段里可同时支持 http 与 https 服务,像这样:

listen   80;
listen   443 ssl;

配置证书:

ssl_certificate      /etc/nginx/zhigang.net.crt;
ssl_certificate_key  /etc/nginx/zhigang.net.key;

证书现在免费的 StartSSL 挺好的,大部分浏览器都支持,新版 Linux 也都内置了 StartSSL 的 CA 证书。只是 java 目前还没有内置。具体申请与使用回头单写。

启用 ssl session 缓存:

ssl_session_cache shared:SSL:10m;
ssl_session_timeout  10m;

第一条设置一10M大小的缓存池,因为 Nginx 使用本地内存,所以这在分布式系统中作用就不大了。第二条缓存时间,单位分钟。

加密协议与算法:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_prefer_server_ciphers on;

因 SSL3.0 及以下版本已被爆出各种漏洞,现在已极不推荐使用,所以我们只启用 TLS 系列协议。

加密算法部分涉及东西太多,这里先注意需禁用的几个:MD5 RC4 DES 及 NULL。这里列出的算法规则应该是比较优化的。

启用 OCSP

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/startssl_trust_chain.crt;
resolver 8.8.8.8 8.8.4.4;

简单点说,启用 OCSP 可以让浏览器更快的获取证书撤销状态,提高加载速度。ssl_trusted_certificate 是 CA 的证书链,如 StartSSL class 1 证书这样生成:

wget https://www.startssl.com/certs/ca-sha2.pem
wget https://www.startssl.com/certs/class1/sha2/pem/sub.class1.server.sha2.ca.pem
cat sub.class1.server.sha2.ca.pem ca-sha2.pem > startssl_trust_chain.crt

启用 HSTS

add_header Strict-Transport-Security "max-age=31536000";

用于通知浏览器强制使用 https 通信,这样下次用户在直接输入域名访问时,浏览器会自动使用 https 请求,避免一次跳转。

Notes

完成设置后可以使用测试网站:https://www.ssllabs.com/ssltest/ 测下自己的得分,本站经过优化前的 D 提升到了 A+。

Ubuntu双线双IP配置

软件环境

Ubuntu 14.04

网络环境

两条网络,电信与移动,测试服务器位于内网,移动IP:192.168.1.8,电信IP:192.168.23.8。

路由器使用端口映射对外暴露部分服务。

设置网络

/etc/network/interfaces

auto em1
#iface em1 inet dhcp
iface em1 inet static
address 192.168.1.8
gateway 192.168.1.1
netmask 255.255.255.0
broadcast 192.168.1.255
network 192.168.1.0

auto em2
iface em2 inet static
address 192.168.23.8
netmask 255.255.255.0

em1 使用移动网络,服务器默认出口。em2 使用电信网络,不设置网关。

设置路由表,实现原路返回数据

修改:/etc/iproute2/rt_tables 文件,增加条记录

252 mobile
251 telecom

执行命令:

ip route flush table telecom
ip route add default via 192.168.23.1 dev em2 src 192.168.23.8 table telecom
ip rule add from 192.168.23.8 table telecom

将以上命令加入 /etc/rc.local 实现重启配置不丢失。

在这个案例中,移动为默认网络,所以不需要特殊配置路由表,否则会导致网络不通。

设置路由表,实现指定网络访问特定内容

route add -host 91.189.95.83 gw 192.168.23.1

91.189.95.83 是 launchpad.net 源的IP地址,移动访问不稳定,这里指定网关使用电信网络访问。

结语

网上很多教程是错误的,主要在实现原路返回数据部分,很多人都说要写两个网络的路由表,但这会直接导致网络不通,我也在这里卡了很久。

OpenStack安装笔记

折腾一个礼拜,终于算把OpenStack搭起来了。

教程:https://github.com/ist0ne/OpenStack-Grizzly-Install-Guide-CN/blob/OVS_Quantum_MutliNode/OpenStack_Grizzly_Install_Guide.rst

安装OpenVSwitch时遇到了点问题,模块编译通不过,后来发现Ubuntu内核版本为3.8,应该是OpenVSwitch还不支持,改用3.2版内核后成功安装。

虚拟机默认CPU是QEMU的虚拟CPU,性能极差,修改 /etc/nova/nova.conf,增加 libvirt_cpu_mode = host-passthrough 可直接使用物理机CPU,达到最大性能。但注意,只有KVM支持host-passthrough。详细说明:https://wiki.openstack.org/wiki/LibvirtXMLCPUModel

几个有用的资源:

陈沙克日志:http://www.chenshake.com/ 国内OpenStack大牛

puppet-openstack: https://github.com/stackforge/puppet-openstack Puppet部署模块,正式环境大规模部署我计划用这个

Ubuntu OpenStack包地址: http://ubuntu-cloud.archive.canonical.com/ubuntu/dists/precise-updates/ 包括最新的Havana测试版也在,回头试试。

批量转换文件编码

适用于Linux及 Mac OS 系统,Windows就抱歉了,你们自己想办法吧。

find . -name *.java -exec sh -c "iconv -f GBK -t UTF8 {} > /tmp/iconv.tmp" \; -exec mv /tmp/iconv.tmp '{}' \;

以上命令将所有 .java 文件由GBK编码转换为UTF8编码。

注意,网上有很多文章将 iconv 命令写成这样:

find . -name *.java -exec sh -c "iconv -f GBK -t UTF8 {} > {}" \;

再说一句,上面这种写法是错误的,以上写法相当于 iconv 输出到源文件,但不知是BUG还是别的原因,你只能得到0字节的空文件,所以必须使用第一条命令,先生成到一个临时文件,然后再移动回来。

JAVA+jni 包名或方法名中含有下划线(_)的解决方法

jni或NDK定义C函数是用下划线(_)作为命名分隔。但如果JAVA包名或方法名里包含了_ 那么就会发生找不到函数的错误,对此网上的各教程都说不要使用 _,对于新项目,这种情况当然很容易避免,但若对一原有项目开发jni接口就麻烦了,特别是在 Android 平台下,包名决定着应用的唯一识别,轻易改不的。

经过不懈的努力,我终于找到了解决方法,简单来说就是在 _ 后加1,如:

// JAVA
package net.zhigang.ndk_test;

public class NdkTest
{
    static {
        System.loadLibrary(ndktest);
    }
    public native void sayHello();
}

//C
#include <jni.h>

jstring Java_net_zhigang_ndk_1test(JNIEnv* env, jobject thiz)
{
    return env->NewStringUTF("Hello from jni");
}

注意上面C语言部分的 ndk_1test。

移植libcurl到Android NDK

测试通过环境:
操作系统:Mac OS
NDK:NDK r8e
libcurl:7.31.0

下载NDK环境: http://developer.android.com/tools/sdk/ndk/index.html ,安装环境不废话了。

在NDK安装目录执行

./build/tools/make-standalone-toolchain.sh

导出编译工具,一般生成压缩如 /tmp/ndk-zhigang/arm-linux-androideabi-4.6.tar.bz2。

解压:

cd /opt/
sudo tar xf /tmp/ndk-zhigang/arm-linux-androideabi-4.6.tar.bz2

下载curl源代码:http://curl.haxx.se/download.html

设置编译环境:

export LDFLAGS="\
-L/usr/local/Cellar/android-ndk/r8e/platforms/android-14/arch-arm/usr/lib"

export CPPFLAGS="\
-I/usr/local/Cellar/android-ndk/r8e/platforms/android-14/arch-arm/usr/include"

将以上路径修改为自己的NDK安装目录。

进入curl目录:

./configure --host=arm-linux-androideabi \
--disable-ftp \
--disable-gopher \
--disable-file \
--disable-imap \
--disable-ldap \
--disable-ldaps \
--disable-pop3 \
--disable-proxy \
--disable-rtsp \
--disable-smtp \
--disable-telnet \
--disable-tftp \
--without-gnutls \
--without-libidn \
--without-librtmp \
--without-ssl \
--disable-dict

mark

生成的静态库文件位于:lib/.libs/libcurl.a 动态库:libcurl.so.5.3.0

一般我们使用静态库文件。

复制 libcurl.a 到项目 jni/ 目录,修改 Android.mk 文件:

# A simple test for the minimal standard C++ library
#

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := curl
LOCAL_SRC_FILES := libcurl.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_CFLAGS = -Wno-psabi
LOCAL_MODULE := curltest
LOCAL_SRC_FILES := curltest.c
LOCAL_STATIC_LIBRARIES := libcurl
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_LDLIBS    += -lz

参考:http://stackoverflow.com/questions/11330180/porting-libcurl-on-android-with-ssl-support

终于上了一次电视

年前受《命运60秒》节目组的邀请,去北京录了一期节目,河南卫视每周二21:20播,我录那期在这个月16号播了,悲剧的是我没看到直播…

好在现在都有点播可看,我在大约34分钟左右,刚上台有些紧张,好在上台后问答环节总算没丢人,欲知最后我成功没有?自己看吧,呵呵。

用了 https 视频没办法 iframe 过来了,地址:http://v.youku.com/v_show/id_XNTQzOTI2MDQ0.html

开始使用vim作主要开发工具

经过这么多年,IDE与编辑器用了不少,从开始的EditPlus到ZendStudio再到Eclipse、NetBeans、TextMate、Sublime Text等等。

几年前也曾经尝试过VIM、Emacs,但当时一是年少无知,二是插件没有如今这么丰富与强大,特别是在VIM支持Python、Ruby等脚本语言开发插件后,插件功能日益强大,甚至可以达到IDE标准,还有UltiSnips实现了类似TextMate的tab功能。加上灵活的自定义功能,让经过几年漂泊的我发现这几十年前诞生的编辑器竟如此强大。所以就这样了,选定离手。

下面是一些目前我使用的插件:

  • vundle (最好用的VIM插件管理工具,当年插件安装复杂也是我无法继续的原因之一)
  • UltiSnips (仿TextMate的TAB补全,支持Python写snips)
  • NERDTree (项目目录树)
  • NERTCommenter (代码注释,支持多种语言)
  • TagBar (显示类方法、函数列表、属性等导航)
  • neocomplcache (代码提示)
  • vim-octopress (本博客代码加亮)
  • phpcomplete.vim (PHP代码自动完成)
  • javacomplete (JAVA代码自动完成)
  • zencoding.vim (HTML快速书写)
  • numbers.vim (在命令模式显示与当前行间隔的行数,用于快速定位)
  • powerline (漂亮的状态栏)
  • matchit (快速找到标签的开始或结束位置)
  • AutoClose (自动关闭括号、引号)

我的.vimrc配置已经发布到GitHub: https://github.com/yinzhigang/dotvim

下载themeforest.net的主题

themeforest.net有很多漂亮的主题,但都是收费的而且还不便宜。但幸好很多主题都提供预览功能,这时我们就可以使用wget神器将HTML、CSS、JS、图片等下载下来,当然Wordpress之类的主题就需要自己写程序来实现功能了。

比如这个简洁的后台管理模板,我们点 Live Prefiew 找到真正的预览地址:http://benblogged.com/dev/ninja_admin/

wget -mk http://benblogged.com/dev/ninja_admin/

稍等一会儿,包括CSS图片在内的文件就会被下载到benblogged.com/dev/ninja_admin/,这个页面比较简单,只有一个HTML,其他复杂些也一样。

如果对方服务器有限制,如限制抓取速度,那么:

wget -mk -w 20 http://benblogged.com/dev/ninja_admin/

-w 20 代表每隔20秒下载一个文件。

还有服务器限制User-Agent,这也好办:

wget -mk -U 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11' http://benblogged.com/dev/ninja_admin/

这就模拟成了普通浏览器,怎么样,酷吧。