定义
当多个线程拜访某个类时,不论运行时环境采纳何种调度形式或者这些线程将如何交替执行,并且在调用代码中不须要任何额定的同步或者协同,这个类都能体现出正确的行为,那么就称这个类是线程平安的。
以上定义十分简单明了,稍后肯定会查明出处。
如果非要给以上定义一个解释的话,能够是:多线程环境下,不论调用方有多少个线程、不论以什么样的程序或形式进行调用,调用方不须要关怀、也不须要做任何解决,被调用方可能确保正确行为。那么,这个被调用的类就是线程平安的。
问题的引入
咱们先来看一个多线程调用后,被调用类不能依照预期给出正确后果的例子:
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<threadCount;i++){Thread t = new Thread(new ThreadDemo(acct,countDownLatch));
t.start();}
try {countDownLatch.await();
}catch(Exception e){ }
System.out.println("counter is :" + acct.getCounter());
}
}
定义一个 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)。
通过以上剖析,咱们能够分明地晓得什么是线程平安问题、导致线程平安问题的起因。下一节详细分析如何防止线程平安问题。