上一节我和你们一块儿打到了并发中的恶霸可见性,这一节咱们继续讨伐三恶之一的原子性。html
一个或者多个操做在 CPU 执行的过程当中不被中断的特性称为原子性。java
我理解是一个操做不可再分,即为原子性。而在并发编程的环境中,原子性的含义就是只要该线程开始执行这一系列操做,要么所有执行,要么所有未执行,不容许存在执行一半的状况。数据库
咱们试着从数据库事务和并发编程两个方面来进行对比:编程
原子性概念是这样子的:事务被当作一个不可分割的总体,包含在其中的操做要么所有执行,要么所有不执行。且事务在执行过程当中若是发生错误,会被回滚到事务开始前的状态,就像这个事务没有执行同样。(也就是说:事务要么被执行,要么一个都没被执行)安全
原子性概念是这样子的:多线程
从上文中对原子性的描述能够看出,并发编程和数据库二者之间的原子性概念有些类似: 都是强调,一个原子操做不能被打断!!并发
而非原子操做用图片表示就是这样子的:ide
线程A在执行一下子(尚未执行完成),就出让CPU让线程B执行。这样的操做在操做系统中有不少,牺牲切换线程的极短耗时,来提升CPU的利用率,从而在总体上提升系统性能;操做系统的这种操做就被称为“时间片”切换。post
经过序中描述的原子性的概念,咱们总结出了:致使共享变量在线程之间出现原子性问题的缘由是上下文切换。性能
那么接下来,咱们经过一个例子来重现原子性问题。
首先定义一个银行帐户实体类:
@Data
@AllArgsConstructor
public static class BankAccount {
private long balance;
public long deposit(long amount){
balance = balance + amount;
return balance;
}
}复制代码
而后开启多个线程对这个共享的银行帐户进行存款操做,每次存款1元:
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
/**
* @author :mmzsblog
* @description:并发中的原子性问题
* @date :2020/2/25 14:05
*/
public class AtomicDemo {
public static final int THREAD_COUNT = 100;
static BankAccount depositAccount = new BankAccount(0);
public static void main(String[] args) throws Exception {
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < THREAD_COUNT; i++) {
Thread thread = new DepositThread();
thread.start();
threads.add(thread);
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Now the balance is " + depositAccount.getBalance() + "元");
}
static class DepositThread extends Thread {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
depositAccount.deposit(1); // 每次存款1元
}
}
}
}复制代码
屡次运行上面的程序,每次的结果几乎都不同,偶尔能获得咱们指望的结果100*1*1000=100000元
,以下是我列举的几回运行结果:
出现上面状况的缘由就是由于
balance = balance + amount;复制代码
这段代码并非原子操做,其中的balance是一个共享变量。在多线程环境下可能会被打断。就这样原子性问题就赤裸裸的出现了。如图所示:
固然,若是balance是一个局部变量的话,即便在多线程的状况也不会出现问题(可是这个共享银行帐户不适用局部变量啊,不然就不是共享变量了,哈哈,至关于废话),由于局部变量是当前线程私有的。就像图中for循环里的j变量。
可是呢,即便是共享变量,小编我也毫不容许这样的问题出现,因此咱们须要解决它,而后更加深入的理解并发编程中的原子性问题。
局部变量的做用域是方法内部,也就是说当方法执行完,局部变量就被抛弃了,局部变量和方法同生共死。而调用栈的栈帧也是和方法同生共死的,因此局部变量放到调用栈里那儿是至关的合理。而事实上,局部变量的确是放到了调用栈里。
正是由于每一个线程都有本身的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,因此天然也就没有并发问题。总结起来就是:没有共享,就不会出错。
但此处若是用局部变量的话,100个线程各自存1000元,最后都是从0开始存,不会累计,也就失去了本来想要展示的结果。故此方法不可行。
正如此处使用单线程也能保证原子性同样,由于不适合当前场景,所以并不能解决问题。
在Java中,对基本数据类型的变量的读取和赋值操做是原子性操做。
好比下面这几行代码:
// 原子性
a = true;
// 原子性
a = 5;
// 非原子性,分两步完成:
// 第1步读取b的值
// 第2步将b赋值给a
a = b;
// 非原子性,分三步完成:
// 第1步读取b的值
// 第2步将b值加2
// 第3步将结果赋值给a
a = b + 2;
// 非原子性,分三步完成:
// 第1步读取a的值
// 第2步将a值加1
// 第3步将结果赋值给a
a ++; 复制代码
把全部java代码都弄成原子性那确定是不可能的,计算机一个时间内能处理的东西永远是有限的。因此当无法达到原子性时,咱们就必须使用一种策略去让这个过程看上去是符合原子性的。所以就有了synchronized。
synchronized既能够保证操做的可见性,也能够保证操做结果的原子性。
某个对象实例内,synchronized aMethod(){}
能够防止多个线程同时访问这个对象的synchronized方法。
若是一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。
因此,此处咱们只须要将存款的方法设置成synchronized的就能保证原子性了。
private volatile long balance;
public synchronized long deposit(long amount){
balance = balance + amount; //1
return balance;
}复制代码
加了synchronized后,当一个线程没执行完加了synchronized的deposit这个方法前,其余线程是不能执行这段被synchronized修饰的代码的。所以,即便在执行代码行1的时候被中断了,其它线程也不能访问变量balance;因此从宏观上来看的话,最终的结果是保证了正确性。但中间的操做是否被中断,咱们并不知道。如需了解详情,能够看看CAS操做。
PS:对于上面的变量balance你们可能会有点疑惑:变量balance为何还要加上volatile关键字?其实这边加上volatile关键字的目的是为了保证balance变量的可见性,保证进入synchronized代码块每次都会去从主内存中读取最新值。
故此,此处的
private volatile long balance;复制代码
也能够换成synchronized修饰
private synchronized long balance;复制代码
由于而这都能保证可见性,咱们在第一篇文章诡异的并发之可见性中已经介绍过了。
public long deposit(long amount) {
readWriteLock.writeLock().lock();
try {
balance = balance + amount;
return balance;
} finally {
readWriteLock.writeLock().unlock();
}
}复制代码
Lock锁保证原子性的原理和synchronized相似,这边不进行赘述了。
可能有的读者会好奇,Lock锁这里有释放锁的操做,而synchronized好像没有。其实,Java 编译器会在 synchronized 修饰的方法或代码块先后自动加上加锁 lock() 和解锁 unlock(),这样作的好处就是加锁 lock() 和解锁 unlock() 必定是成对出现的,毕竟忘记解锁 unlock() 但是个致命的 Bug(意味着其余线程只能死等下去了)。
若是要用原子类定义属性来保证结果的正确性,则须要对实体类做以下修改:
@Data
@AllArgsConstructor
public static class BankAccount {
private AtomicLong balance;
public long deposit(long amount) {
return balance.addAndGet(amount);
}
}复制代码
JDK提供了不少原子操做类来保证操做的原子性。好比最多见的基本类型:
AtomicBoolean
AtomicLong
AtomicDouble
AtomicInteger复制代码
这些原子操做类的底层是使用CAS机制的,这个机制保证了整个赋值操做是原子的不能被打断的,从而保证了最终结果的正确性。
和synchronized相比,原子操做类型至关因而从微观上保证原子性,而synchronized是从宏观上保证原子性。
上面的2.5解决方案中,每一个小操做都是原子性的,好比AtomicLong这些原子类的修改操做,它们自己的crud操做是原子的。
那么,仅仅是将每一个小操做都符合原子性是否是表明了这整个构成是符合原子性了呢?
显然不是。
它仍然会产生线程安全问题,好比一个方法的整个过程是读取A-读取B-修改A-修改B-写入A-写入B
;那么,若是在修改A完成之后,失去操做原子性,此时线程B却开始执行读取B操做,此时就会出现原子性问题。
总之不要觉得使用了线程安全类,你的全部代码就都是线程安全的!这总归都要从审查你代码的总体原子性出发。就好比下面的例子:
@NotThreadSafe
public class UnsafeFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNum = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
@Override
public void service(ServletRequest request, ServletResponse response) {
BigInteger tmp = extractFromRequest(request);
if (tmp.equals(lastNum.get())) {
System.out.println(lastFactors.get());
} else {
BigInteger[] factors = factor(tmp);
lastNum.set(tmp);
lastFactors.set(factors);
System.out.println(factors);
}
}
}复制代码
虽然它所有用了原子类来进行操做,可是各个操做之间不是原子性的。也就是说:好比线程A在执行else语句里的lastNumber.set(tmp)
完后,也许其余线程执行了if语句里的lastFactorys.get()
方法,随后线程A才继续执行lastFactors.set(factors)
方法更新factors
!
从这个逻辑过程当中,线程安全问题就已经发生了。
它破坏了方法的读取A-读取B-修改A-修改B-写入A-写入B
这一总体过程,在写入A完成之后其余线程去执行了读取B,就致使了读取到的B值不是写入后的B值。就这样原子性就出现了。
好了,以上内容就是我对并法中的原子性的一点理解与总结了,经过这两篇文章咱们也就大体掌握了并发中常见的可见性、原子性问题以及它们常见的解决方案。
贴一段常常看到的原子性实例问题。
问:常听人说,在32位的机器上对long型变量进行加减操做存在并发隐患,究竟是不是这样呢?
答:在32位的机器上对long型变量进行加减操做存在并发隐患的说法是正确的。
缘由就是:线程切换带来的原子性问题。
非volatile类型的long和double型变量是8字节64位的,32位机器读或写这个变量时得把人家咔嚓分红两个32位操做,可能一个线程读了某个值的高32位,低32位已经被另外一个线程改了。因此官方推荐最好把longdouble 变量声明为volatile或是同步加锁synchronize以免并发问题。
欢迎关注公众号:Java学习之道
我的博客网站:www.mmzsblog.cn