前面我们有说过,并发问题的源头是可见性、原子性和有序性。本篇就介绍两个保证并发正确的基本原语,synchronized和volatile。
synchronized
synchronized是解决Java原子性、可见性问题的基本原语,是一种同步锁。被synchronized修饰的代码,同一时间段只有一个线程可以执行。执行完成后,对其他线程可见。
synchronized的用法
synchronized作为同步锁,可以修饰的对象有以下三种方式:
修改一个代码块,被修饰的代码块称为同步语句块,其保护的范围是大括号{}括起来的代码,锁是synchronized的参数;
修饰一个方法,被修饰的方法称为同步方法,其保护的范围是整个方法,锁是这个方法的this对象;
修饰一个静态的方法,其作用的范围是整个静态方法,锁是这个类;
synchronized的基本使用示例:
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}synchronized关键字与我们平台认识的锁不一样,没有Lock对象,也没有Lock.lock、Lock.unlock方法。但其本质上,就是同步锁,Lock对象和lock/unlock方法都是隐式的。以上文中修饰代码块的例子来说,换成Lock的伪代码就是:
Object obj = new Object();
void baz() {
obj.lock();
// 临界区
obj.unlock();
}锁与受保护资源的关系
使用任何锁,我们都需要明白两个问题:我们的锁是什么?我们的锁保护的资源是什么?
锁和被保护的资源的是有对应关系的。类比生活中的例子,火车票,火车座位是一种互斥资源,同一个时刻只能有一个人占用,一张火车票对应一个位置。这里的火车票就是锁,拿到了票就是拿到了锁,独占座位(互斥资源),直到到站车锁失效。
synchronized锁也是这样的,在本篇第一节,我们也写了使用synchronized时,锁是什么,保护的资源是什么。
锁和受保护资源一个正确的关系是:受保护的资源与锁之间的N:1的关系,即一把锁可以保护多种资源,但一种资源,只能被一把锁保护。
我们用一个例子看一下这种关系。以下是一个错误的事例:
class SafeCalc {
static long value = 0L;
synchronized long get() {
return value;
}
synchronized static void addOne() {
value += 1;
}
}这段代码中用两把锁this对象和SafeClc.class,保护了同一个对象value。因此,get与addone方法不是互斥的,这就会导致并发问题。
volatile
volatile并不是Java独有的,在C语言中也有此关键字。
volatile用来修饰变量,对于这个变量的操作,不要用CPU缓存,必须从内存中进行读取或写入。这样做的目的是解决可见性问题。达到的效果是一个线程对volatile变量的修改对其他后续线程可见。
使用volatile要注意两个问题:
volatile并不解决原子性问题,因此
volatile long count=0;后,两个线程做count++,得不到正确的结果;volatile的使用很简单,但是和锁机制共同使用,就有复杂的可见性传递问题,这种问题需要对JMM(Java内存模型)及其happen-before规则有一定的了解才可以(具体在附录中介绍);
基于于以上两点,我们建议:使用volatile修饰的变量的修改操作,不能依赖其读操作;使用volatile修饰的变量,不要和其他变量有依赖关系。比如,只用volatile修饰用户配置。





































苏公安网备 32011402010933号