多线程编程要解决的一个基本问题是:共享资源的竞争。而基本上使用并发模式在解决这个问题都采用序列化访问共享资源的方法。基本原理就是当共享资源被一个任务使用时,在其上加锁,其他任务在资源被解锁之前,无法访问它。在任务对其解锁后,另一个任务就可以锁定并使用它。下面看看Java支持的线程同步机制。
1.synchronized关键字
synchronized关键字即可应用于对象相关的同步,也可用于类层次的同步(static属性);
对象上应用synchronized可以实现对象方法的同步和代码块的同步。
在对象方法上应用synchronized形式为:
synchronized void functionName()。
当在对象上调用被synchronized修饰的方法,此对象都被加锁,这时该对象上其他synchronized方法只有等前一个方法调用完毕并且释放锁之后才能被调用。一个任务可以多次获取对象的锁(例如在执行synchronized方法内部,又调用该对象的第二个方法,后者又调用对象的第三个方法。释放锁时,直到最后一个synchronized方法调用完毕,才真正释放锁)。在并发编程中,将域设为private很重要,否则synchronized关键字就不能阻止其他任务访问域了。
如果想得到更小的同步的粒度,可以将synchronized用于同步方法内部的部分代码,形式为:
synchronized(syncObject) {
//花括号内部为要同步的代码块,也成为临界区
}
在进入临界区之前,必须要获得syncObject对象的锁,最合理的做法是:使用方法被调用的对象作为syncObject:synchronized(this)。相比于同步整个方法,同步控制块的性能更好,因为对象不加锁的时间更长。
将synchronized用于修饰static方法可以在类范围内防止对static数据的访问。
何时要进行同步呢?同步规则:当你正在写一个变量,他可能接下来将被另一个线程读取,或者正在读取一个上一此已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。这叫Brian的同步规则。
2.原子性与易变性
原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前(切换到其他线程)执行完成。Java内置的基本类型,除了64位的long和double,读取和写入操作都是原子性的。因为JVM会将64位的读取和写入操作分离为两个32位的操作,这个过程中间可能产生线程切换。要获取long或者double的原子性操作,要使用volatile关键字。原子操作由线程机制来确保其不可中断,可以利用这种特性编写无锁的代码(尽管如此,还是不建议使用原子性操作来替代同步)。注意在Java中,递增和递减操作(++/--)不是原子操作。
可视性问题:在多核操作系统中,一个任务作出的修改,即使是原子性的,对其他任务来说也可能是是不可视的(修改只是临时存储在本地处理器的缓存)。但是同步机制强制一个任务做出的修改必须在应用中是可视的。volatile关键字还能提供可视性保证。如果将一个域(field)声明为volatile,那么只要对这个域产生了写操作,那么所有的读操作都能看到这个修改。
注意原子性与可视性的差异:在非volatile域的原子操作,不必刷新到主存中,因此其他读取该域的任务也不必看到这个新值。原子性不代表、不确保可视性。
使用原则: 如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,应该将这个域设置为volatile。
3.使用显式的Lock对象
Java SE5起新增的显式互斥机制。Lock对象必须显式地创建、锁定和释放。例如:
public class LockTest {
private int currentVal = 0;
private Lock lock = new ReentrantLock();
public int timed() {
boolean locked = false;
try {
System.out.println("timed....");
locked = lock.tryLock(2, TimeUnit.SECONDS);
++currentVal;
return currentVal;
}
catch (InterruptedException e) {
e.printStackTrace(System.out);
return -1;
}
finally {
if(locked)
lock.unlock();
System.out.println("finish timed.... locked="+locked);
System.out.println("currentVal="+currentVal);
}
}
public static void main(String[] args) throws Exception{
ExecutorService executorService = Executors.newCachedThreadPool();
final LockTest syncObject = new LockTest();
executorService.execute(new Runnable() {
public void run() {
try {
syncObject.nextInt();
}catch (Exception e) {
e.printStackTrace(System.out);
}
}
});
executorService.execute(new Runnable() {
public void run() {
syncObject.timed();
}
});
executorService.shutdown();
}
}
输出:
nextInt....
timed....
finish timed.... locked=false
currentVal=1
finish nextInt....
currentVal=2
使用定时获取锁操作,在规定时间内没有获取到锁对象,程序将照样往下执行,导致不同步发生。比较好的实现是:1.尝试获取锁超时后,再次尝试;2.报告超时作为返回。