定义
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
以上定义非常简单明了,稍后一定会查明出处。
如果非要给以上定义一个解释的话,可以是:多线程环境下,不管调用方有多少个线程、不管以什么样的顺序或方式进行调用,调用方不需要关心、也不需要做任何处理,被调用方能够确保正确行为。那么,这个被调用的类就是线程安全的。
问题的引入
我们先来看一个多线程调用后,被调用类不能按照预期给出正确结果的例子:
public class Account {
private int counter=0;
public void doAddCounter(){
for(int j=0;j<100;j++){
counter++;
}
}
public int getCounter(){
return counter;
}
}
public class ThreadDemo implements Runnable {
CountDownLatch countDownLatch;
private Account acct;
public ThreadDemo(Account acct,CountDownLatch countDownLatch) {
this.acct = acct;
this.countDownLatch=countDownLatch;
}
@Override
public void run() {
try {
acct.withdrawal(BigDecimal.valueOf(10));
acct.doAddCounter();
Thread.sleep(15);
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
}
public class DemoApplication {
public static void main(String[] args) {
Account acct=new Account();
int threadCount = 1000;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
System.out.println(countDownLatch);
for(int i=0;i
定义一个Account类,只包含一个成员变量 counter,doAdd方法调用counter++对成员变量counter累加100次。
然后再定义一个线程类ThreadDemo,为了实现对Account类的多线程调用,线程执行时直接调用Account的doAdd方法对counter进行累加。
然后在DemoApplication中启用1000个ThreadDemo线程。
期望的结果很明显:我们在主线程中DemoApplication中实例化了一个Account对象acct,然后启用1000个线程分别调用acct对象的doAdd方法,每一个线程对acct的counter累加100次,得到的结果应该就是100。那么1000个线程执行完成后,结果就应该是 counter=100*1000=100000。
执行结果与我们的期望不符:
第一次执行:
counter is :99929
第二次执行:
counter is :99936
而且多次执行,结果不一样。
无法得到预期结果的原因,是因为Account类不是线程安全的,多线程并发时不能保证正确的结果。
以下我们把不具备线程安全性导致的多线程问题称为线程安全问题。
线程安全问题的原因
引发线程安全问题的原因:
- CPU调度导致多个线程抢占系统资源。
- 多个线程对抢占资源的操作不具备原子性。
- 指令重排。
网上查找到的导致java线程安全问题的原因还有很多,但是以上前两条就足够我们清楚的分析导致线程安全问题的底层原因了。
所以,本文先分析前两条。
多线程抢占系统资源
我们日常碰到的绝大多数线程安全问题,绝大部分都是因为多线程并发时,各线程抢占内存资源时导致的。
为了聚焦到:搞清楚多线程问题的原因,我们只需要简单了解一下JMM(Java Memery Model)的工作原理,就足够了。我们不需要对整个JMM做透彻了解,JMM的内容足够你单独花时间学习一阵子了。。
首先简单分析一下JMM:
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
我们需要明白以下最重要的两点即可:
- 类成员变量分配在JMM的堆内存,多线程共享。
- 方法变量分配在JMM的栈内存,线程独占。
很容易的,我们能知道:多线程争夺共享资源的时候,才有可能导致线程安全问题,线程独占的变量不可能导致线程安全问题!
原子性操作
原子性操作的概念:不可被中断的一个或一系列操作。
不可被中断的意思就是,一系列操作执行的过程中,该线程不允许被中断,因此也就不允许其他线程插入执行其他操作。
所以,很容易的能得出结论:针对某一内存变量的操作具备原子性的话,该操作就不可能导致线程安全问题。
线程安全问题的原因
综合以上两点,我们现在可以得出导致线程安全问题的必要条件为:
- 多线程争夺共享变量。
- 对该变量的计算操作不具备原子性。
对线程安全例子的再次分析
现在我们用以上得出的结论,重新分析文章开头提出的线程安全问题的例子。
该例子中导致线程安全问题的变量是Account类的成员变量counter,对该变量的操作是Account的doAdd()方法:counter++。
由于counter是Accout类的成员变量,我们知道成员变量是在多线程之间共享内存的,所以,满足线程安全问题的第一个条件。
那么,counter++这个操作具备原子性吗?很不幸,counter++这个操作不具备原子性。
为了分析清楚这个问题,我们需要简单了解一下CPU对内存的调用机制:由于CPU运算速度远远大于内存读取速度,所以,为了提高效率,内存数据会首先读取到CPU缓存(或者叫寄存器),计算完成后再写回到内存中。
counter++这个操作被分解为:
- 从内存读取到寄存器。
- 执行加1操作。
- 从寄存器写回内存。
所以我们可以看到counter++在底层不是一个操作,而是一系列操作,并且不是原子性的,在任何步骤都有可能被CPU中断。
我们可以设想一下导致以上线程安全问题的执行过程:
- 某一时刻counter在内存中的值为100。
- 线程1执行counter++,读取counter的值100到寄存器,然后加1得到counter的值为101,在写回内存前线程1被中断。
- 线程2执行counter++,读取counter的值(此时线程1的执行结果尚未写回内存,所以,counter的值仍然为100),然后加1得到counter1的值为101,写回内存,counter的值为101。
- 线程1被唤醒,counter值101写回内存,counter得到错误的结果101(本来应该是102)。
通过以上分析,我们可以清楚地知道什么是线程安全问题、导致线程安全问题的原因。下一节详细分析如何避免线程安全问题。