很多程序的复杂性都是由于并发引起的,如果在并发情况下,如果对象是无状态那可以很好的运行,无论是在单线程还是多线程环境下都没问题,但是很多时候对象的状态是需要上下文维护的,因此在并发情况下就很容易导致不一致性的情况。我们先看下面一个例子:
class File {
private int num;
public void incr() throws InterruptedException, BrokenBarrierException {
this.num ++;
}
public int getNum() {
return num;
}
}
测试代码:
for (int j = 0;j < 100;j ++) {
// 测试100次
final ExecutorService executorService = Executors.newCachedThreadPool();
Set<Integer> set = new HashSet<Integer>();
final File file = new File();
// 启动1000个线程
for (int i = 0;i < 1000;i ++) {
executorService.execute(new Runnable () {
@Override
public void run() {
try {
file.incr();
} catch (Exception ex) {
}
}
});
}
executorService.shutdown();
while (true) {
// 等待线程任务执行完毕
if (executorService.isTerminated()) {
System.out.println(file.getNum());
break;
}
}
}
就是模拟多个线程去对num进行自增。线程安全性的定义是怎样这个没一个标准的定义,但是最广泛的说法就是与单线程执行保持一致性,包括数据库的最高事务隔离级别也是可串行化级别。如果这个操作是单线程执行,最后的结果必然是1000。运行100次也是同样的结果。但是在并发的情况下,这里i ++操作是非原子操作,这个操作实际上是分三步,读出i值,给i加1,赋值给i,然后将i写会主内存。这里需要介绍的内存模型,如图:
线程读取到数据进行操作的时候是在线程私有内存完成,因此可能出现这种情况:
两个线程同时并发读取对象i,然后在不同的栈内存内修改完值写会主内存会导致修改丢失。串行化的结果应该是i被增加了2,这就是非线程安全。
上面代码运行情况在高并发情况下很容易出现 num < 1000的情况。
归根到底是因为i ++并非原子操作。这个在jdk可以采用AtomicInteger类完成原子递增。这样:
class File {
private AtomicInteger num = new AtomicInteger(0);
public void incr() throws InterruptedException, BrokenBarrierException {
num.addAndGet(1);
}
public int getNum() {
return num.get();
}
}
这样自增相当于是一个原始操作,可以达到类似于以下效果:
class File {
private Integer num = new Integer(0);
private Object lock = new Object();
public void incr() throws InterruptedException, BrokenBarrierException {
synchronized (this.lock) {
// 其他线程阻塞到之前
this.num = this.num + 1;
}
}
public int getNum() {
return num;
}
}
保证自增为原子操作。值得注意的是我在测试的时候发现一个问题,如果是对Integer对象加锁:
synchronized (this.num) {
this.num = this.num + 1;
}
则不能保证其他线程阻塞在this.num = this.num + 1;之前。无法完成同步。.
当然除了采用Java内置锁还可以采用并发包提供的lock。例如:
private Integer num = new Integer(0);
private ReentrantLock lock = new ReentrantLock();
public void incr() throws InterruptedException, BrokenBarrierException {
try {
this.lock.lock();
this.num ++;
} catch (Exception ex) {}
finally {
this.lock.unlock();
}
}
一样能够保证自增的原子性。如果不采用锁机制:
private volatile Integer num = new Integer(0);
这样:
public void incr() throws InterruptedException, BrokenBarrierException {
this.num ++;
}
能够保证线程安全吗?线程安全包含两个要素,操作的原子性和可见性,volatile只能保证可见性而不能保证操作的原子性,因为不会采用锁机制。它的原理是如果多个线程栈内存缓存了某个值,如果其中一个线程修改了这个值,会立刻更新到共享内存,也会通知其他线程该变量指向的值是失效的,其他线程将不会使用该线程缓存的值而是强制去刷共享内存的值。
线程 : 读取i == 1 - >修改 i = 2 - > 写回内存(立即刷新共享内存)
volatile并不阻碍另一个线程去获取i的值。因此还是可能读到修改前的数据,无法保证线程安全性。
加锁机制:
线程1:读取i == 1 - >修改 i = 2 - > 写回内存
线程2:阻塞 - ——————————-> 读取i = 2 ..写回内存3
线程3 : 阻塞 - ————————————————————>读取i == 3
因此上述代码不是线程安全的。
总而言之volatile只能保证更新的数据能够立刻更新到共享内存,定义变量为volatile尽量不要让更新后的值依赖之前的值。当然由于 volatile不会加锁,因此会具有更好的并发性能。