在并发的场景下,很容易出现线程安全的问题。使用synchronized可以很方便地解决此问题。
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
@Override
public int next() {
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
} /* Output:
Press Control-C to exit
595 not even!
597 not even!
*/
EvenGenerator在并发执行的时候,由于
- 是对同一个实例的成员变量操作;
- ++currentEvenValue是非原子操作(在之前的文章有讲到:java的同步语法volatile和synchronized);ps:这一点不必要,即使换成了AtomicInteger进行自增,也有线程安全问题;
- next方法里涉及多步操作;
第三点非常重要,很容易出现一个线程将另一个线程的currentEvenValue覆盖或者进行了额外的自增操作,导致得到不符合预期的结果。
synchronized
用synchronized就可以解决上面的问题
public class SynchronizedEvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
@Override
public synchronized int next() {
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new SynchronizedEvenGenerator());
}
}
为了使并发的情况更容易出现,特意加了一行Thread.yield()调用,线程执行到这行的时候,会将控制权让出去(php的yield相关:zan框架入门(一)——协程)。
之前的文章里讲过synchronized,这里不再赘述。
Lock对象
Lock是java的并发包里提供的功能,并非原生支持。
ReentrantLock是比较常用的Lock类,基于它同样可以解决并发安全问题,虽然逻辑上要比synchronized复杂一些:
public class MutexEvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
private Lock lock = new ReentrantLock();
@Override
public int next() {
lock.lock();
try {
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
EvenChecker.test(new MutexEvenGenerator());
}
}
注意return必须在unlock之前调用,否则仍然会有并发安全问题。
可以看到,虽然用Lock需要更多的代码,但是更加灵活,适用于特殊的定制化场景。
一般的,如果可以用synchronized解决的,就直接使用synchronized。某些特殊的场景,比如并发逻辑块抛出异常要进行清理,或者持有锁一段时间后再释放。