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