Java中的原子性与并发编程总结

问题起源

来自知乎的一个问题引起了我的关注,问题地址是:https://www.zhihu.com/question/42779759

在java doc 中 https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible.
求教各位大神,这个在java中怎么发生这个错误?

我认为该问题的实质是,java中的原子性操作并不能满足多线程访问共享变量的需求,因此还是要使用synchronize等并发手段来进行多线程间的共享变量访问。

原子性和可见性

首先给出两个定义:原子性和可见性。
原子性:In programming, an atomic action is one that effectively happens all at once. An atomic action cannot stop in the middle: it either happens completely, or it doesn’t happen at all. No side effects of an atomic action are visible until the action is complete.简单的说,原子性的操作就是不可被中断的操作,这个操作要么还未开始,要么全部完成。
可见性:可见性是指,一个线程对变量的修改,另一个线程能够立即看到。

原子性操作的分类

Java中的原子性操作,我将其总结为四类:

  1. 原始类型:除long和double意外的原始类型的简单读取、写入操作;所谓简单写入,是指i=1这种赋值操作,而i++不是简单写入;
  2. volatile:使用volatile修饰的变量的简单读取、写入操作;同样的,volatile int i的i++操作不是简单写入;
    3.原子类: java.util.concurrent.atomic包中各种原子类(例如AtomicInteger、AtomicBoolean)的读取、写入操作,注意incrementAndGet这种自增操作具有原子性;
  3. 并发锁:使用synchronize或者Lock进行限定的并发锁,其中的代码都具有原子性。

分析

我对这四种类型就原子性、可见性和性能进行了分析,给出如下表格:(我将i++定义为“自增操作”)

类型 原子性 简单读写的原子性和可见性 自增操作的原子性和可见性 复杂代码的原子性和可见性 性能
原始类型 Y N(无可见性) N(既无原子性也无可见性) N
volatile Y Y N(无原子性) N 较高
原子类 Y Y Y 难以实现
并发锁 Y Y Y Y

结论

由上表可得出以下结论:

  1. 原始类型虽然对简单读写保持原子性,但是由于没有可见性,在共享资源被多线程访问时基本无用;
  2. volatile变量可对简单读写保持原子性和可见性,因此在较为极端的情况下可用于共享资源的多线程访问,《Thinking in java》一书中曾经提到“如果要使用volatile来修饰多线程的共享资源,除非该对象中仅有一个域的值能够变化,其赋值时不能引用自身的值,也不能添加一些数值的比较条件”。这种要求实在是太苛刻了,一般的自增操作就会打破此规则。因此volatile在绝大多数情况下仍是不可用的;
  3. 原子类的应用领域则宽广得多,由于原子类可对自增操作保持原子性和可见性,因此常常被用来作为多线程中指示数值的共享资源。且原子类的性能要优于并发锁,毕竟它省去了加锁、解锁过程。
  4. 在除了上面2和3提到的一些特殊情况外,其他时间若多线程要访问共享资源,则最好使用并发锁对资源访问代码进行加锁。

    总结起来就是:在并发编程且访问共享变量时,经常用并发锁,明白时用原子类,尽量不用volatile。

你可能感兴趣的:(编程技巧,Java技术)