天天看点

Java并发编程之原子性问题什么是原子性问题简单总结参考

目录

  • 什么是原子性问题
    • 举例说明一下
    • 怎么解决
      • 自带原子性保证
      • synchronized 和 Lock锁
      • 原子操作类型
      • 最好的方法还是使用无锁编程
  • 简单总结
  • 参考

什么是原子性问题

原子性是指在一个操作中,cpu不可以在中途暂停然后再调度,要么一次执行完成,要么就不执行。
在Java中当我们讨论一个操作具有原子性问题,是一般就是指这个操作会被线程的随机调度打断而产生的一系列的问题。

举例说明一下

我们先来看一些例子,来了解什么是原子性的操作

a = true;  //原子性
a = 5;     //原子性
a = b;     //非原子性,分两步完成,第一步加载b的值,第二步将b赋值给a
a = b + 2; //非原子性,分三步完成
a ++;      //非原子性,分三步完成:1、读取a的值,2、计算a的值+1,3、赋值
           

接下类我们看一个由于原子性导致的问题:

public class Singleton {  
	private static Singleton singleton;  
	private Singleton (){}  
	public static Singleton getSingleton() {  
		if (singleton == null) {	//判空
			synchronized (Singleton.class) {	//加锁  
	        if (singleton == null) {  	//再次判空
	        	singleton = new Singleton();  
	        }  
	    }  
 	}  
       return singleton;  
    }  
} 
           

以上是使用双重校验锁实现的一个单例,按照常理,只能由一个线程可以执行

new Singleton();

的操作,但是比较诡异的是,在一些时候,以上单例会出现空指针的异常。虽然大部分时间是正常的,但是只要出现了问题,那么编码上一定是有问题的。

下面我们来分析为什么会出现这样的问题:

  • 第一个线程判空成功之后,使用synchronized关键字对赋值操作进行加锁,第二次判空之后开始了创建对象并赋值的操作
  • 注意,这里的操作并不是原子性的,简单来说,这里的操作可以分成三步:
    1. 创建内存空间
    2. 在内存空间内创建对象
    3. 将内存空间赋值给变量singleton
  • 上面的操作由于不是原子性,所以三步之间是可以被中断的,再加上不是原子操作,所以可能会进行重排序,所以又产生了有序性问题
  • 如果将步骤2和步骤3进行了重排序,创建完内存之后立刻赋值,赋值之后再进行对象的创建,而另外一个线程在赋值和对象的创建之间对变量singleton 进行了访问,那么他就会拿到一个半成品的对象。 这时就会出现空指针异常。

这个问题是由顺序性和原子性两个原因共同导致的。

所以synchronized关键字对原子性的保证是从结果上保证的,因为对于整个赋值操作,无论是否重排序,确实没有影响结果,但是对于另外一个线程来讲就不尽相同了。

摘抄一段

不是说synchronized是可以保证有序性的么,这里为什么就不行了呢?
首先,可以明确的一点是:synchronized是无法禁止指令重排和处理器优化的。那么他是如何保证的有序性呢?
这就要再把有序性的概念扩展一下了。Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。
以上这句话也是《深入理解Java虚拟机》中的原句,但是怎么理解呢?周志明并没有详细的解释。这里我简单扩展一下,这其实和as-if-serial语义有关。
as-if-serial语义的意思指:不管怎么重排序,单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。
这里不对as-if-serial语义详细展开了,简单说就是,as-if-serial语义保证了单线程中,不管指令怎么重排,最终的执行结果是不能被改变的。
那么,我们回到刚刚那个双重校验锁的例子,站在单线程的角度,也就是只看Thread1的话,因为编译器会遵守as-if-serial语义,所以这种优化不会有任何问题,对于这个线程的执行结果也不会有任何影响。

淦,跑题了

怎么解决

自带原子性保证

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作。

synchronized 和 Lock锁

这两个方法可以通过加锁的方法保证单线程中最终的结果具有原子性,但是不能保证中间的操作是否被打断

原子操作类型

public static class BankAccount {
    //省略其他代码
    private AtomicDouble balance;

    public double deposit(double amount) {
        return balance.addAndGet(amount);
    }
    //省略其他代码
} 
           

JDK提供了很多原子操作类来保证操作的原子性。原子操作类的底层是使用CAS机制的,这个机制对原子性的保证和synchronized有本质的区别。CAS机制保证了整个赋值操作是原子的不能被打断的,而synchronized值能保证代码最后执行结果的正确性,也就是说synchronized能消除原子性问题对代码最后执行结果的影响。

最好的方法还是使用无锁编程

提到托线程编程,就绕不开锁,但是锁本身又是性能杀手,所以就出现了无锁编程这一庞大而又复杂的话题。而上面所说的CAS机制就是一种无锁编程的应用。

但是CAS我还没学会,挖个坑吧

简单总结

在多线程编程环境下(无论是多核CPU还是单核CPU),对共享变量的访问存在原子性问题。这个问题可能会导致程序错误的执行结果。JMM主要提供了如下的方式来保证操作的原子,保证程序不受原子性问题的影响。

  1. synchronized机制:保证程序最终正确性,是的程序不受原子性问题的影响;
  2. Lock接口:和synchronized类似;
  3. 原子操作类:底层使用CAS机制,能保证操作真正的原子性。

参考

程序员自由之路

HollisChuang’s Blog » 既生synchronized,何生volatile