就算是有几年工作经验的,如果没有专业的训练,也不一定能写出一手线程安全的代码,对于一般的web开发而言,多线程相关的部分都封装在web server里了,而平时的业务开发不涉及这些高级特性。这是一件好事,因为这样将程序员的注意力都集中在与公司收入直接相关的业务逻辑层,而不需要关注比较复杂的技术层面,但是对程序员个人提升上也有不利的一面,通用的复杂技术都被封装了,程序员工作的技术性也相应降低。所以这需要我们在业余时间不断充电,训练,并且在工作上把握一切提升自我的机会。
以前做过一个爬虫项目,每天要抓取大量的商品数据,但是一些知名电商网站往往会设置各种限制,其中一个限制就是ip黑名单,网站会识别一些有爬虫机器特征的访问来源ip,并计入黑名单,下次爬取就会设置各种关卡,其中一个应对方法就是动态变更ip,方法如下:
public int changeIPBySh() {
try {
logger.error("execute shell command ");
Worker.execute();
} catch (Exception e) {
logger.error("Error when process changeIp work" + e.getMessage(), e);
}
return 0;
}
原有程序是通过shell脚本变更ip地址的,由于是多线程运行的,经常会有多个线程同时执行脚本的情况,我们很快就会想到加锁。
public int changeIPBySh() {
try {
synchronized(this){
logger.error("execute shell command ");
Worker.execute();
}
} catch (Exception e) {
logger.error("Error when process changeIp work" + e.getMessage(), e);
}
return 0;
}
但是这并没有什么卵用,当一个线程执行完脚本解锁后,原来在对象锁等待的线程会获得锁,进而再次执行脚本,这导致一些无谓的ip变更,而且在变更过程中,会影响其他线程的内容抓取。我们的目标是保证在同一时刻只有一个线程变更ip,变更时,新的线程不再等待释放锁,也不重复执行变更脚本。tryLock就可以实现这一目标。
private int changeIPByShdd() {
boolean captured = lock.tryLock();
try {
if (captured) {
logger.error("execute shell command ");
Worker.execute();
} else {
Thread.sleep(sleepTime);
}
} catch (Exception e) {
logger.error("Error when process changeIp work" + e.getMessage(), e);
} finally {
if (captured) {
lock.unlock();
}
}
return 0;
}
使用tryLock,如果获取了锁则返回true,执行脚本,如果没有,则立即返回false,线程进入休眠。而使用synchronized则会一直等待锁的释放,在语义tryLock提供了一种更适合当前场景的机制。
从广泛的层面而言,使用synchronized,一旦发生死锁,只能重启应用,而tryLock却可以避免一些偶发的死锁。synchronized是在jvm层实现的,发生了异常会自动释放锁,但是tryLock是在代码层面实现的,需要自己释放锁:
finally {
if (captured) {
lock.unlock();
}
}