多线程关于无锁的线程是否安全问题

Posted 桃花键神

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程关于无锁的线程是否安全问题相关的知识,希望对你有一定的参考价值。

🔎这里是JAVA多线程加油站
👍如果对你有帮助,给博主一个免费的点赞以示鼓励
欢迎各位🔎点赞👍评论收藏⭐️

文章目录

无锁

  • 就人的性格而言,可以分为乐天派和悲观派。对于乐天派来说,他们总是会把事情往好的方面想。他们认为所有事情总是不太容易发生问题,出错是小概率的,因此可以大胆地做事。如果真的不幸遇到了问题,则努力解决问题。而对于悲观的人来说,他们总是担惊受怕,认为出错是一种常态,所以无论大小事情都考虑得面面俱到,为人处世,确保万无一失。
  • 对于并发控制而言,锁是一种悲观的策略。它总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心翼翼。如果有多个线程同时需要访问临界区资源,则宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。而无锁是一种乐观的策略,它会.假设对资源的访问是没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行。那遇到冲突怎么办呢?无锁的策略使用一种叫作比较交换(CAS,Compare And Swap)的技术来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

与众不同的并发策略:比较交换

  • 与锁相比,使用比较交换会使程序看起来更加复杂一些,但由于其非阻塞性,它对死锁问题天生免疫,并且线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
  • CAS算法的过程是:它包含三个参数CAS(V,E,N),其中V表示要更新的变量,E表示预期值,N表示新值。仅当√值等于E值时,才会将V的值设为N,如果V值和E值不同,说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
  • 简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,则说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。
  • 在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK5以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且这种操作在虚拟机中可以说是无处不在的。

无锁的线程安全整数:Atomiclnteger

  • 为了让Java程序员能够受益于CAS等CPU指令,JDK并发包中有一个atomic包,里面实现了一些直接使用CAS操作的线程安全的类型。
  • 其中,最常用的一个类就是AtomicInteger,可以把它看作一个整数。与Integer不同,它是可变的,并且是线程安全的。对其进行修改等任何操作都是用CAS指令进行的。这里简单列举一下AtomicInteger
    的一些主要方法,对于其他原子类,操作也是非常类似的。


就内部实现上来说,AtomicInteger中保存了一个核心字段:


它就代表了AtomicInteger 的当前实际取值。此外还有一个:

  • 它保存着value字段在AtomicInteger对象中的偏移量。后面你会看到,这个偏移量是实现AtomicInteger的关键。

AtomicInteger的使用非常简单,这里给出一个示例:

  • 第6行的AtomicIntegerincrementAndGet()方法会使用CAS操作将自己加1,同时也会返回当前值(这里忽略了当前值)。执行这段代码,你会看到程序输出了100000。这说明程序正常执行,没有错误。如果不是线程安全,那么i的值应该会小于100 000才对。
  • 使用AtomicInteger会比使用锁具有更好的性能。由于篇幅限制,这里不再给出AtomicInteger和锁的性能对比的测试代码,相信写一段简单的代码测试两者的性能应该不是难事。这里让我们关注一下
    incrementAndGet()方法的内部实现(基于对JDK 1.7的分析可知,JDK 1.8与JDK 1.7的实现有所不同)。


其中get(方法非常简单,就是返回内部数据value。

  • 这里让人印象深刻的应该是incrementAndGet()方法的第2行for循环吧!如果你是初次看到这样的代码,可能会觉得很奇怪,为什么连设置一个值那么简单的操作都需要一个死循环呢?原因就是:CAS操作未必是成功的,因此对于不成功的情况,我们就需要不断地进行尝试。第3行的get()取得当前值,接着加1后得到新值next。这里,我们就得到了CAS必需的两个参数:期望值及新值。使用compareAndSet()方法将新值next写入,成功的条件是在写入的时刻,当前的值应该要等于刚刚取得的current。如果不是这样,则说明AtomicInteger的值在第3行到第5行代码之间又被其他线程修改过了。当前线程看到的状态就是一个过期状态。因此,compareAndSet返回失败,需要进行下一次重试,直到成功。
  • 以上就是CAS操作的基本思想,无论程序多么复杂,其基本原理总是不变的。
  • 和AtomicInteger类似的类还有:AtomicLong用来代表long型数据;AtomicBoolean表示boolean型数据;AtomicReference表示对象引用。

Java中的指针: Unsafe类

  • 如果你对技术有追求,应该还会特别在意incrementAndGet()方法中compareAndSet(方法的实现。现在,就让我们进一步看一下它吧!

  • 在上面的代码中,我们看到一个特殊的变量unsafe,它是
    sun.misc.Unsafe类型。从名字看,这个类应该是封装了一些不安全的操作。那什么操作是不安全的呢?学习过C或者C++,大家应该知道,指针是不安全的,这也是在Java中把指针去除的重要原因。如果指针指错了位置,或者计算指针偏移量时出错,结果可能是灾难性的,你很有可能会覆盖别人的内存,导致系统崩溃。
  • 而这里的Unsafe类就是封装了一些类似指针的操作。
  • compareAndSwapInt()方法是一个navtive方法,它的几个参数含义如下:

  • 第一个参数o为给定的对象,offset为对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),expected表示期望值,x表示要设置的值。如果指定的字段的值等于expected,那么就把它设置为x。

  • 不难看出,compareAndSwapInt()方法的内部,必然是使用CAS原子指令来完成的。此外,Unsafe类还提供了一些方法,主要有以下几种(以int操作为例,其他数据类型是类似的)﹔

  • 如果大家还记得“3.3.4高效读写的队列:深ConcurrentLinkedQueue类”一节中描述的ConcurrentLinkedQueue类实现,应该对ConcurrentLinkedQueue类中的Node还有些印象.Node的一些CAS操作也都是使用Unsafe类来实现的。大家可以回顾一下,以加深对Unsafe类的印象。

  • 这里就可以看到,虽然Java抛弃了指针,但是在关键时刻,类似指针的技术还是必不可少的。这里底层的Unsafe类实现就是最好的例子。但是很不幸,JDK的开发人员并不希望大家使用这个类。获得Unsafe类实例的方法是调动其工厂方法getUnsafe(),但是它的实现却是这样的:

  • 注意加粗部分的代码,它会检查调用getUnsafe()函数的类,如果这个类的ClassLoader不为null,就直接抛出异常,拒绝工作。因此,这也使得我们自己的应用程序无法直接使用Unsafe类。它是一个在JDK内部使用的专属类。

注意:根据Java类加载器的工作原理,应用程序的类由App Loader 加载。而系统核心类,如rt.jar中的类由Bootstrap类加载器加载。Bootstrap类加载器没有Java对象的对象,因此试图获得这个类加载器会返回null。所以,当一个类的类加载器为null时,说明它是由 Bootstrap类加载器加载的,而这个类也极有可能是rt.jar中的类。

本文参考多线程高并发程序设计,非常棒的一本书,推荐xdm学习

以上是关于多线程关于无锁的线程是否安全问题的主要内容,如果未能解决你的问题,请参考以下文章

单插入多读取列表是否安全无锁?

Java 多线程分析----CAS操作和阻塞

017 无锁与CAS

Linux:详解多线程(线程池读写锁和CAS无锁编程)

什么是非无锁的无阻塞算法示例?

多线程安全问题及各种锁