1. java多线程中的原子性的理解
众所周知,原子是构成物质的基本单位(当然电子等暂且不论),所以原子的意思代表着——“不可分”。
由不可分性可知,原子性是拒绝多线程操作的,因此不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作(其他线程的读操作也不行,因为要防止脏读)!
以创建对象为例:Person per = new Person();,看似一步创建对象的操作,但是对于计算机底层操作来说,这一步实际上可以分解成以下三个步骤:
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
但是需要注意,创建对象操作不是原子性操作,对于多线程操作,在一个线程创建对象执行上述三个步骤的过程中,可能会有其他线程来查询该对象的数据,但在该对象还没有彻底初始化完毕时就查询数据,可能查询到的数据是脏数据,不是最新数据。
下面说一下同步锁和原子操作的区别:
synchronized 是不是能够代替原子操作?不能, 因为synchronized同步代码块在执行的过程当中,它内部的变量可以被其他线程获取到。而如果用原子操作,当该变量被一个线程操作时,其他线程不能同时操作该变量(读操作也不行,因为要防止脏读)。这个区别就造成了,如果有需求要求synchronized同步代码块内的变量不能被其他线程获取到,此时就要用原子操作。 但是貌似这种需求比较少。
下面说一下java多线程下集合类不安全原因:List.add(),Set.add(),Map.add(),i++等,导致数据不安全的问题的原因解释如下:
以i++为例进行举例说明,其他同理
java中i++操作不具有原子性,不是原子操作,其实际上分为三步:
- 获得 i 值
- +1
- 写回这个值。
Time | Thread A | Thread B |
---|---|---|
T1 | 获得 i 值 | |
T2 | 获得 i 值 | |
T3 | i–>+1 | |
T4 | i–>+1 | |
T5 | 写回这个值 | |
T6 | 写回这个值 |
上表中线程A和线程B再各自的工作内存中得到的是相同的 i 值,因此经过这两个线程+1操作并写回主内存的是相同的值,A、B两个线程操作i++后 i 只是加了一次1,线程A的结果被线程B的结果覆盖了,这就是因为i++不具有原子性,而导致的数据安全问题。
解决上述问题可通过加锁或利用原子类来解决,对于集合类有CopyOnWriteArrayList
, CopyOnWriteArraySet
, ConcurrentHashMap
分别对应List,Set,Map的三个类来解决集合类的多线程数据安全问题,而对于i++则可利用原子类AtomicInteger
来解决,原子类底层用的是C++来直接操作内存并利用CAS的构成自旋锁,可通过查看原码得知。
java中的原子类具有原子性,其可保证原子性,java中的原子类在JUC包下:java.util.concurrent.atomic
2. 数据库事务中的原子性的理解
**事务举例:**A想要从自己的帐户中转1000块钱到B的帐户里。那么从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:
-
- 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
-
- 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电、断网什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚(ROLLBACK)。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。
**事务的原子性:**如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行,而且一个事务在执行过程中,其他事务想要操作同一数据(读操作也不行,因为要防止脏读),就需要等待该事务执行完毕。
原子操作: 符合原子性的操作就是原子操作。