目录
一、线程安全
1、什么是线程安全
虽然多线程编程极大地提高了效率,但是也会带来一定线程安全的隐患。
举个例子:现在有两个线程分别从网络上爬取数据,然后插入一张数据库表中,要求不能插入重复的数据。
在插入数据的过程中存在两个操作:1)先检查数据库中是否存在该条数据;2)如果存在,则不插入;如果不存在,则插入到数据库中。
假如两个线程分别用thread-1和thread-2表示,某一时刻,thread-1和thread-2都读取到了数据X,那么可能会发生这种情况:因为thread-1和thread-2同时开启,thread-1去检查数据库中是否存在数据X和thread-2也去检查数据库中是否存在数据X是同时发生的。结果两个线程检查的结果都是数据库中不存在数据X,那么两个线程都分别将数据X插入数据库表当中。
所以在单线程中就不会出现线程安全问题,thread-1先去数据库中检查没有数据X,等到thread-1插入完数据后thread-2才能开启。而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会存在一个问题:由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。
Java Concurrency in Practice中是这么描述线程安全的:
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其它的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
也就是说:
- 线程安全是和对象密切绑定的;
- 线程的安全性是由于线程调度和交替执行造成的;
- 线程安全的目的是实现正确的结果
我们还可以通过jvm内存管理的角度去理解线程安全问题:
数据的存储相对于cpu运算能力需要消耗大量时间,为了充分利用运算能力引入了缓存。cpu计算时数据读取顺序优先级:寄存器->高速缓存->内存,计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。这个适当的时机就能确保缓存的一致性。
Java内存模型规定了所有的变量都存储在主内存(Runtime Data Area 的 Heap)中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须是工作内存中进行,也就是操作的是主内存副本拷贝而不能直接读写主内存中的变量。线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。当多个线程同时读写某个内存数据时,各个线程都从主内存中获取数据,线程之间数据是不可见的,就可能会产生寄存器/高速缓存/内存之间的同步问题。
2、如何避免线程安全问题
从JVM层面避免线程安全问题主要围绕着并发过程中的原子性、可见性、有序性这三个特征(有点类似数据库中事务的ATOM的四个特性)。
2.1、原子性
原子性就是操作不能被线程调度机制中断,要么全部执行完毕,要么不执行。
比如:k=i++; 就不具有原子性,如果把这行代码看成线程的话,那么其中涉及到两个操作,先将i的值赋给k,然后i进行自增1。另外在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的&#x