011 线程安全性问题
一 . 概述
在之前,我们说到线程安全性问题是我们在并发设计首要考虑的问题.
那么到底什么是并发问题呢?
看下面的例子(经典的例子):
public class Problem { private int count = 0; public static void main(String[] args) throws Exception { Problem demo = new Problem(); Thread t1 = new Thread(new Runnable() { @Override public void run() { for(int x =0;x<10000;x++) { demo.add(); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for(int x =0;x<10000;x++) { demo.add(); } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("获取最终的结果count : " + demo.count); } private void add() { count++; } }
上面的代码描述了两个线程共同将一个count自增,每人1000次,但是最后运行的结果让我们大失所望,几乎每次都不是2000次.
二 .问题的分析
其实最根本的问题就是两个线程获取的count的状态不一致导致的,也就是说count++操作根本就不是原子性的操作,
这就造成了线程可以获取到一个另外一个线程的中间状态.
这可能很抽象,简单说,对count的操作应该是顺序的.
三 .安全性问题发生的条件
在上面我们展示了安全性问题,但是安全性问题何时会发生呢?毕竟,如何我们的程序中如果没有安全性问题,我们就减少了并发时所考虑的问题,这可以帮助我们简化问题.
线程安全性发生的条件:
[1]多线程并发情况
[2]竞争相同资源
[3]对竞争资源进行非原子操作[常见的就是读写不一致]
解释一个上面的条件:
前面两个不去解释了.就对第三个条件进行解释.
原子性:
这个原子性的概念和我们在事务中的概念是一致的,那就是我们的操作应该是一个完整的个体,要么不发生,要么完全发生.在其中,不会被打断.
拿上面的例子来说,count++这个操作就不是原子性的,因为这个语句在底层会分成多个语句执行,每个语句都是可以被打断的.
四 .问题的解决
线程安全性问题的解决最常见的方法就是同步,当然同步的方式有很多种,
比如加锁,synchronized,CAS等方式,但是他们的核心都是一点,保证原子性.
五 . JVM的内存结构
java为了更快的提供运行速度,在每一个线程栈之中设置了缓存,由于缓存之中的不一致就会造成并发的安全性问题.
常常出现的就是这样的一个情景,一个线程将共享变量进行了修改,但是另外一个线程却还是使用的之前缓存之中的内容,也就是说,一个线程对共享变量的修改无法及时的反映到另外一个线程之中,这就是可见性问题.
我们一般可以使用volatile关键词来完成线程的可见性问题.
六 .指令重排序
jvm为了加速程序的执行,会对我们的代码进行指令重排序.这样造成的后果就是后面的代码未必比之前的代码后执行,也就是说代码的执行顺序是不确定的.
好像这种情况对我们造成了灾难性的后果,但是jvm为了解决这个问题,提供了happens-before的规范,jvm定义了一些列的执行顺序的规范,也就是说,.一些代码必须在另外一些代码之前运行.
我们可以通过happens-before来完成指令重排序的推导.
但是,我们更加常见的问题就是发生在共享变量访问的时候,此时不确定的指令重排序就可能造成安全性问题.
我们常用的解决方法就是happens-before中加锁的规范,加锁的代码一定保证指令的顺序性,这样我们就在一定的程度上抵消了指令重排序带来的问题.