首页 课程 师资 教程 报名

深入理解原子性操作,提高多线程高并发性能

  • 2019-09-07 09:00:00
  • 2668次 动力节点

  我们在过去传统的编程过程中,我们定义一个内存中的数据类型,用于将数据放入内存中时,只考虑被单个线程访问,所以不需要考虑太多,因为它是安全的。

  但当数据被多个线程同时操作时,就会出现问题。我们必须同步多个线程对数据的访问,以确保可见性和正确性。

  因为JMM模型中对于多线程并发操作时,父线程中共享的资源变量是存储在父线程的内存空间里的,而作为子线程,各自有各自的线程空间,我们也称之为工作线程的内存空间,每次操作都会拷贝共享资源的副本到自己的工作线程并操作,在操作完成后,需要将各个工作线程的副本更新会主线程的共享变量中,这个过程造成了各个线程可能读取的共享变量数值可能不一致问题。同时一个子线程对共享数据的修改,在被更新会父线程空间的变量值时,不能被其它子线程立刻看到,这就出现了操作可见性问题。

  比如我们定义一个普通的计数器数据类型Counter类:

image.png

  这个类在单线程环境中工作是没有任何问题的,但是当多个线程访问同一个计数器实例时就完全不能工作了。因为并发操作的每个线程都会拷贝它一个副本到自己的线程空间里进行操作,完成后更新会共享的变量中,这就有可能出现数据不一致问题。

  同步锁操作实现

image.png

  此时我们可以使用对操作方法使用synchronized关键字或者使用volatile关键字进行同步的方式来解决问题。

  这个进行了同步的类可以很好的解决了多个线程并发访问它的问题。但是我们知道Synchronized关键字背后的实现原理是对修饰的方法进行锁定处理,而且这个加锁和解锁过程并不是一个轻量级的机制,所有存在很多缺点。

  当多个线程在试着获取同一个锁时,大部分线程都会被挂起,在锁被释放时才会被重置回来。

  当我们的操作临界区比较小,这个开销就会变得非常大,尤其是当锁经常被获取并且存在很多争用时。

  另一个缺点在于其它线程在等待锁被释放过程中什么也做不了,如果拥有当前锁的线程执行出现延迟(页面错误或者量子钟结束等原因),那么其它线程就会出现长时间得不到执行的情况。

  原子操作实现原理

  为了避免这些问题的出现,Java推出了非锁定的算法。该算法不使用锁机制,而且更具可伸缩性和更好的性能。

  它使用了底层的机器指令的原子性来确保高层操作的原子性。简单说来就是将高一层级的操作分解成跟底层原子操作层级对应的操作,从而让每一次操作不能被中断或者窃取。

  我们知道在处理资源争用时我们一般有两种思路:

  一种是悲观模式,就是相信争用的情况一定会发生,所以预先建立防范措施,其中加锁就是属于这种悲观模式的实现。

  另外一种是乐观模式,我们沿着每次操作都可能会成功也可能会失败的思路处理,就是如果成功就执行完毕,如果出现失败就再执行一次。

  悲观模式是不管怎样我先锁定资源使用的是一种阻塞思维,而原子操作则是不采用锁定机制,大家都可以访问的非阻塞思维。

  目前,实际的处理器大都提供了一些指令,它们大大简化了这种非阻塞算法的实现,其中目前使用最多的操作是比较和交换操作(CAS)。

  这种算法的操作原理是它接收三个参数分别为要操作的内存地址,我们预期的当前要操作的数值,我们需要它变成的新值。

  它会首先检查指定内存地址的值是否跟当前我们预期的值相同,如果相同就将它更新为新值,如果跟我们预期的值不同就放弃本次操作,当本次操作完成时,它会返回指定内存地址的值。因此,当多个线程试图执行CAS操作时,一个线程获胜,其他线程什么也不做。调用者可以选择重试或执行其他操作。

  其实我们平时经常使用这种比较和设置的操作原理来实现一些其它功能操作,方法原理与CAS完全相同,只是会返回一个布尔值,指示操作是否成功。

  可用的原子操作类型

  在Java5.0之前,开发人员不能直接使用这个操作,但是在Java5.0中添加了几个原子变量(int、long、boolean和引用变量)。

  int和long版本也支持数字操作。JVM使用了有硬件机器,CAS或者使用锁的Java实现来编译这些类从而使之成为更好的操作方式。

  AtomicInteger

  AtomicLong

  AtomicBoolean

  AtomicReference

  它们都通过compareAndSet()等方法支持CAS操作,所以它们支持多线程访问,并且比同步操作有着更好的可伸缩性。

image.png

  这里incrementAndGet()和decrementAndGet()方法是AtomicLong和AtomicInteger类提供的两个数值操作。

  另外还有getAndDecrement(),getAndIncrement(),getAndAdd(inti)和addAndGet()等可以调用的安全操作。

  这个版本比同步版本更快,而且线程安全。

  最后,我们只使用compareAndSet()方法实现了一个遵循CAS模式的increment()方法。

  这看起来有些复杂,但这就是实现非阻塞算法的代价。其思想就是当检测到冲突时,我们会重试,直到操作成功。

  这是非阻塞算法的通用模式,我们在非阻塞流NIO操作中会经常看到这种模式的使用。

  自定义复杂原子操作类型

  下面我们举一个实例来实现一个Stack类:

image.png

image.png

  它确实比在这两个方法上使用synchronized要复杂,但是如果存在争用,它的性能也会更好,即使没有争用的情况下,它的性能通常也是不错的。

  总之,原子变量类是实现非阻塞算法的一种很好的方法,而且也是volatile变量的一个很好的替代方法,因为它们可以提供原子性和可见性。

以上就是动力节点java培训机构介绍的“深入理解原子性操作,提高多线程高并发性能”的内容,希望对正在学习的你有所帮助,如果有不懂的问题可以登录动力节点IT培训官网咨询在线客服老师。

选你想看

你适合学Java吗?4大专业测评方法

代码逻辑 吸收能力 技术学习能力 综合素质

先测评确定适合在学习

在线申请免费测试名额
价值1998元实验班免费学
姓名
手机
提交